From 875ddd80d6d58f9308b6d18e745c47a983c1ae3c Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Fri, 12 Apr 2024 20:04:29 +0300 Subject: [PATCH 001/125] SMP proxy: protocol (#954) * WIP: proxy-related types * test plan * buildable with stubs * add auth test * update protocol * fix * update rfc * update protocol/types * disable test --------- Co-authored-by: Evgeny Poberezkin --- notes-flow.txt | 23 +++++++ rfcs/2023-09-12-second-relays.md | 87 +++++++++++++------------ simplexmq.cabal | 1 + src/Simplex/Messaging/Crypto.hs | 2 + src/Simplex/Messaging/Protocol.hs | 80 ++++++++++++++++++++++- src/Simplex/Messaging/Server.hs | 23 +++++++ src/Simplex/Messaging/Server/Env/STM.hs | 45 ++++++++++--- src/Simplex/Messaging/Server/Main.hs | 3 +- src/Simplex/Messaging/Transport.hs | 4 ++ tests/SMPClient.hs | 12 +++- tests/SMPProxyTests.hs | 67 +++++++++++++++++++ tests/ServerTests.hs | 2 - tests/Test.hs | 2 + 13 files changed, 295 insertions(+), 56 deletions(-) create mode 100644 notes-flow.txt create mode 100644 tests/SMPProxyTests.hs diff --git a/notes-flow.txt b/notes-flow.txt new file mode 100644 index 000000000..93f984509 --- /dev/null +++ b/notes-flow.txt @@ -0,0 +1,23 @@ +common: + corrId - random BS, used as CbNonce + entityId - p2r tlsUniq + +# setup +s->p: "proxy", uri, auth? + # unless connected + p->r: "p_handshake" + p<-r: "r_key", tls-signed dh pub +s<-r: "r_key", tls-signed dh pub # reply entityId contains tlsUniq + +# working +s ; generate random dh priv, make shared secret +s->p: s2r("forward", random dh pub, SEND command blob) + p->r: p2r("forward", random dh pub, s2r("forward", ...))) + r->c@ "msg", ... + p<-r: p2r("r_res", s2r("ok" / "error", error)) +s<-p@ s2r("ok" / "error", error) + +# expired + p<-r@ p2r("error", "key expired") +s<-p@ "error", "key expired" +s ; reconnect \ No newline at end of file diff --git a/rfcs/2023-09-12-second-relays.md b/rfcs/2023-09-12-second-relays.md index 3721ee721..a47eb7bde 100644 --- a/rfcs/2023-09-12-second-relays.md +++ b/rfcs/2023-09-12-second-relays.md @@ -2,9 +2,9 @@ ## Problem -SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an audequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency. +SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an adequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency. -The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realise that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product". +The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realize that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product". Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses. @@ -43,7 +43,7 @@ Overall, this is not a viable or even appropriate option for the current stage. 3. SMP / XFTP proxy. -Introduce SMP and XFTP protocol extenstions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users. +Introduce SMP and XFTP protocol extensions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users. Pros: - no dependency on and lower latency than via Tor @@ -68,7 +68,7 @@ Below considers this design. 2. SMP proxy should not be able to observe queue addresses and their count on the destination relays. This requirement is not needed for XFTP proxies, as each file chunk is downloaded only once, so there is no need to hide its address. -3. There must be no identifiers and cyphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality). +3. There must be no identifiers and ciphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality). 4. Traffic between the client and destination relays must be e2e encrypted, with MITM-by-proxy mitigated, relying on the relay identity (certificate fingerprint), ideally without any additional fingerprint in relay address. @@ -97,11 +97,11 @@ This would also reduce the difference in how the traffic looks to the observer - The flow of the messages will be: -1. Client requests proxy to create session with the relay by sending `server` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic. +1. Client requests proxy to create session with the relay by sending `PRXY` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic. -2. Proxy connects to SMP relay, negotiating a shared secret in the handshake that will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). SMP relay also returns in handshake its temporary DH key to agree e2e encryption with the client (sender-relay encryption, to hide metadata sent to the destination relay from proxy). +2. Proxy connects to SMP relay, negotiating a shared secret via a handshake headers - it will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). DH key returned by SMP relay in handshake will also be used to encrypt client commands, combining it with random per-command keys (sender-relay encryption, to hide metadata sent to the destination relay from proxy). -3. Proxy replies with `server_id` command including relay session ID to identify it in further requests, relay DH key for e2e encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before). +3. Proxy replies to sender with `PKEY` message using "entityId" transmission field to indicate session ID for using in further requests, relay DH key for _s2r_ encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before). A possible attack here is that proxy can use this TLS session to replay commands received from the client. Possibly, it could be mitigated with a bloom filter per proxy/SMP relay connection that would reject the repeated DH keys (that need to be used for replay), and also with DH key expiration (this mitigation should allow some acceptable rate of false positives from the bloom filter). @@ -113,11 +113,11 @@ It is important that the same public key from destination relay is returned to a *Unrelated cosideration for SMP protocol privacy improvement*: instead of signing commands to the destination relay, the sender could have a ratchet per queue agreed with the destination relay that would simply use authenticated encryption with per-message symmetric key to encrypt the message on the way to relay, and this encryption would be used as a proof of sender. -4. Now the client sends `forward` to proxy, which it then forwards to SMP relay, applying additional encryption layer. +4. Now the client sends `PFWD` to proxy, which it then forwards to SMP relay as `RFWD`, applying _p2r_ encryption layer. -5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional encryption layer. +5. SMP relay sends `RRES` to proxy applying _p2r_ encryption layer, which it then forwards to the client as `PRES`, removing the _p2r_ encryption layer. -Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session. +Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients' relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session. Sequence diagram for sending the message via SMP proxy: @@ -126,33 +126,33 @@ Sequence diagram for sending the message via SMP proxy: | sending | | SMP | | SMP | | receiving | | client | | proxy | | relay | | client | ------------- ------------- ------------- ------------- - | `server` | | | - | -------------------------> | create TLS session, get keys | | + | `PRXY` | | | + | -------------------------> | | | | | ------------------------------> | | - | `server_id` | (if doesn't exist) | | + | | SMP handshake | | + | | <------------------------------ | | + | `PKEY` | | | | <------------------------- | | | | | | | - | TLS(F:s2r(SEND(e2e(msg)))) | | | - | -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | | + | `PFWD` (s2r) | | | + | -------------------------> | | | + | | `RFWD` (p2r) | | | | ------------------------------> | | - | | | | - | | TLS(R:p2r(s2r(OK/ERR))) | | - | TLS(R:s2r(OK/ERR)) | <------------------------------ | | - | <------------------------- | | TLS(MSG(r2c(e2e(msg)))) | - | | | -----------------------> | - | | | | - | | | TLS(ACK) | + | | `RRES` (p2r) | | + | | <------------------------------ | | + | `PRES` (s2r) | | `MSG` | + | <------------------------- | | -----------------------> | + | | | `ACK` | | | | <----------------------- | | | | | | | | | - ``` -Below diagram shows the encrypttion layers for `forward` and `response` commands: +Below diagram shows the encrypttion layers for `PFWD`/`RFWD` commands and `RRES`/`PRES` responses: -- s2r (added) - encryption between client and SMP relay, with relay key returned in server_id command, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. +- s2r (added) - encryption between client and SMP relay, with relay key returned in relay handshake, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. - e2e (exists now) - end-to-end encryption per SMP queue, with double ratchet e2e encryption inside it. -- p2r (added) - additional encryption between proxy and SMP relay with key agreed in the handshake, to mitigate traffic correlation inside TLS. This key could also be signed by the same certificate, if we don't want to rely on TLS security. +- p2r (added) - additional encryption between proxy and SMP relay with the shared secret agreed in the handshake, to mitigate traffic correlation inside TLS. - r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS. ``` @@ -167,27 +167,32 @@ Below diagram shows the encrypttion layers for `forward` and `response` commands ----------------- ----------------- -- TLS -- ----------------- ----------------- ``` -When proxy connects to SMP relay it would indicate in the handshake that it will use proxy protocol and the SMP relay would expect the same `forward` commands and reply with `response`s. +Question: should proxy declare its role in handshake? When proxy connects to SMP relay it would indicate in the handshake that it will act as a proxy and the SMP relay would expect the same `forward` commands and reply with `response`s. -Below syntax aims to fit in 16kb block using spare capacity in SMP protocol. +Common SMP transmission format (v4), for reference: ```abnf -proxy_block = padded(proxy_transmission, 16384) -proxy_transmission = corr_id relay_session_id proxy_command -corr_id = length *8 OCTET -proxy_command = server / server_id / forward / response / error -server = "S" address [relay_basic_auth] ; creates transport session between proxy and relay -server_id = "I" relay_session_id tls_session_id signed_relay_key ; - ; session_id is the TLS session ID between proxy and relay, it has to be included inside encrypted block to prevent replay attacks -forward = %s"F" random_dh_pub_key encrypted_block ; it's important that a new key is used for each command, to prevent any correlation by proxy or by destination relay -response = %s"R" encrypted_block; response received from the destination SMP relay -relay_session_id = length *8 OCTET -error = %s"E" error +paddedTransmission = +transmission = signature signed +signature = 0 ; empty signatures here +signed = sessionIdentifier corrId entityId (smpCommand / brokerMsg) ``` -The overhead is: 1+8 (corrId) + 1+8 (relay_session_id) + 1 (command) + 1+32 (random_dh_pub_key) + 2 (original length) + 16 (auth tag for e2e encryption) + 16 (auth tag for proxy to relay encryption) = 86 bytes. The reserve for sent messages in SMP is ~84 bytes, so it should about fit with some reduced bytes somewhere. +- `corrId` is fully random each time and used as a nonce for encrypted blocks. +- `entityId` carries tlsUniq from the current proxy-to-relay connection. +- `smpCommand` gets extended with `s2p_command / p2r_command`. +- `brokerMsg` gets extended with `r_key / r_response`. -Another possible design is to allow mixing sent messages and normal SMP commands in the same transport connection, but it can make fitting in the block a bit harder, additional overhead would be: 1 (transmission count) + 2 (transmission size) + 1 (empty signature) = 4 bytes. +```abnf +s2p_command = proxy / forward +p2r_command = p_handshake ; forward is +proxy = %s"PRXY" SP relayUri SP basicAuth +relayUri = length %s"smp://" serverIdentity "@" srvHost [":" port] +forward = %s"PFWD" SP dhPublic SP encryptedBlock +r_key = %s"PKEY" SP dhPublic +r_response = %s"RRES" SP encryptedBlock +dhPublic = length x509encoded +``` The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response). diff --git a/simplexmq.cabal b/simplexmq.cabal index b85281080..95a5578ca 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -651,6 +651,7 @@ test-suite simplexmq-test ServerTests SMPAgentClient SMPClient + SMPProxyTests Util XFTPAgent XFTPCLI diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index 28183a1fc..bffd7559f 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -756,6 +756,8 @@ data Signature (a :: Algorithm) where SignatureEd25519 :: Ed25519.Signature -> Signature Ed25519 SignatureEd448 :: Ed448.Signature -> Signature Ed448 +deriving instance Eq (Signature a) + deriving instance Show (Signature a) data ASignature diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 2c593fc6f..77b14e23c 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -188,7 +188,9 @@ import Data.String import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) +import qualified Data.X509 as X import GHC.TypeLits (ErrorMessage (..), TypeError, type (+)) +import qualified GHC.TypeLits as TE import Network.Socket (ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -359,6 +361,17 @@ data Command (p :: Party) where PING :: Command Sender -- SMP notification subscriber commands NSUB :: Command Notifier + PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- 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 :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- 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 Sender -- use CorrId as CbNonce, proxy to relay deriving instance Show (Command p) @@ -384,6 +397,18 @@ instance Encoding SubscriptionMode where 'C' -> pure SMOnlyCreate _ -> fail "bad SubscriptionMode" +newtype EncTransmission = EncTransmission ByteString + deriving (Show) + +data FwdTransmission = FwdTransmission + { fwdCorrId :: ByteString, + fwdKey :: C.PublicKeyX25519, + fwdTransmission :: ByteString + } + +newtype EncFwdTransmission = EncFwdTransmission ByteString + deriving (Show) + data BrokerMsg where -- SMP broker messages (responses, client messages, notifications) IDS :: QueueIdsKeys -> BrokerMsg @@ -393,6 +418,10 @@ data BrokerMsg where MSG :: RcvMessage -> BrokerMsg NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg + -- Should include certificate chain + PKEY :: X.CertificateChain -> X.SignedExact X.PubKey -> 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 OK :: BrokerMsg ERR :: ErrorType -> BrokerMsg @@ -405,6 +434,17 @@ data RcvMessage = RcvMessage } deriving (Eq, Show) +newtype EncFwdResponse = EncFwdResponse ByteString + deriving (Eq, Show) + +data FwdResponse = FwdResponse + { fwdCorrId :: ByteString, + fwdResponse :: ByteString + } + +newtype EncResponse = EncResponse ByteString + deriving (Eq, Show) + -- | received message without server/recipient encryption data Message = Message @@ -567,6 +607,9 @@ data CommandTag (p :: Party) where DEL_ :: CommandTag Recipient SEND_ :: CommandTag Sender PING_ :: CommandTag Sender + PRXY_ :: CommandTag Sender + PFWD_ :: CommandTag Sender + RFWD_ :: CommandTag Sender NSUB_ :: CommandTag Notifier data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p) @@ -580,6 +623,9 @@ data BrokerMsgTag | MSG_ | NID_ | NMSG_ + | PKEY_ + | RRES_ + | PRES_ | END_ | OK_ | ERR_ @@ -607,6 +653,9 @@ instance PartyI p => Encoding (CommandTag p) where DEL_ -> "DEL" SEND_ -> "SEND" PING_ -> "PING" + PRXY_ -> "PRXY" + PFWD_ -> "PFWD" + RFWD_ -> "RFWD" NSUB_ -> "NSUB" smpP = messageTagP @@ -623,6 +672,9 @@ instance ProtocolMsgTag CmdTag where "DEL" -> Just $ CT SRecipient DEL_ "SEND" -> Just $ CT SSender SEND_ "PING" -> Just $ CT SSender PING_ + "PRXY" -> Just $ CT SSender PRXY_ + "PFWD" -> Just $ CT SSender PFWD_ + "RFWD" -> Just $ CT SSender RFWD_ "NSUB" -> Just $ CT SNotifier NSUB_ _ -> Nothing @@ -639,6 +691,9 @@ instance Encoding BrokerMsgTag where MSG_ -> "MSG" NID_ -> "NID" NMSG_ -> "NMSG" + PKEY_ -> "PKEY" + RRES_ -> "RRES" + PRES_ -> "PRES" END_ -> "END" OK_ -> "OK" ERR_ -> "ERR" @@ -651,6 +706,9 @@ instance ProtocolMsgTag BrokerMsgTag where "MSG" -> Just MSG_ "NID" -> Just NID_ "NMSG" -> Just NMSG_ + "PKEY" -> Just PKEY_ + "RRES" -> Just RRES_ + "PRES" -> Just PRES_ "END" -> Just END_ "OK" -> Just OK_ "ERR" -> Just ERR_ @@ -829,7 +887,7 @@ type family UserProtocol (p :: ProtocolType) :: Constraint where UserProtocol PSMP = () UserProtocol PXFTP = () UserProtocol a = - (Int ~ Bool, TypeError (Text "Servers for protocol " :<>: ShowType a :<>: Text " cannot be configured by the users")) + (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 @@ -1046,6 +1104,8 @@ data ErrorType NO_MSG | -- | sent message is too large (> maxMessageLength = 16088 bytes) LARGE_MSG + | -- | relay public key is expired + EXPIRED | -- | internal server error INTERNAL | -- | used internally, never returned by the server (to be removed) @@ -1135,6 +1195,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg) PING -> e PING_ NSUB -> e NSUB_ + PRXY host auth_ -> e (PRXY_, ' ', strEncode host, ' ', auth_) + PFWD {} -> error "TODO: e (PFWD_,,)" + RFWD {} -> error "TODO: e (RFWD_,,)" where e :: Encoding a => a -> ByteString e = smpEncode @@ -1158,6 +1221,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where PING | isNothing auth && B.null queueId -> Right cmd | otherwise -> Left $ CMD HAS_AUTH + PRXY {} + | isNothing auth && B.null queueId -> Right cmd + | otherwise -> Left $ CMD HAS_AUTH -- other client commands must have both signature and queue ID _ | isNothing auth || B.null queueId -> Left $ CMD NO_AUTH @@ -1189,6 +1255,10 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where Cmd SSender <$> case tag of SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) PING_ -> pure PING + PFWD_ -> error "TODO: PFWD_" + RFWD_ -> error "TODO: RFWD_" + PRXY_ -> PRXY <$> (_smpP >>= either fail pure . strDecode) <*> _smpP + CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1204,6 +1274,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where e (MSG_, ' ', msgId, Tail body) NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta) + PKEY cert key -> e (PKEY_, ' ', C.encodeCertChain cert, C.SignedObject key) + RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) + PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) END -> e END_ OK -> e OK_ ERR err -> e (ERR_, ' ', err) @@ -1221,6 +1294,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP) NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP + PKEY_ -> PKEY <$> (A.space *> C.certChainP) <*> (C.getSignedExact <$> smpP) + RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) + PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) END_ -> pure END OK_ -> pure OK ERR_ -> ERR <$> _smpP @@ -1270,6 +1346,7 @@ instance Encoding ErrorType where CMD err -> "CMD " <> smpEncode err AUTH -> "AUTH" QUOTA -> "QUOTA" + EXPIRED -> "EXPIRED" NO_MSG -> "NO_MSG" LARGE_MSG -> "LARGE_MSG" INTERNAL -> "INTERNAL" @@ -1282,6 +1359,7 @@ instance Encoding ErrorType where "CMD" -> CMD <$> _smpP "AUTH" -> pure AUTH "QUOTA" -> pure QUOTA + "EXPIRED" -> pure EXPIRED "NO_MSG" -> pure NO_MSG "LARGE_MSG" -> pure LARGE_MSG "INTERNAL" -> pure INTERNAL diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 4535c7bd5..456de5be2 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -524,6 +524,9 @@ verifyTransmission auth_ tAuth authorized queueId cmd = Cmd SSender PING -> pure $ VRVerified Nothing -- NSUB will not be accepted without authorization Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (Just q `verifiedWith`) (notifierKey <$> notifier q)) <$> get SNotifier + Cmd SSender PRXY {} -> pure $ VRVerified Nothing + Cmd SSender PFWD {} -> pure $ VRVerified Nothing + Cmd SSender RFWD {} -> pure $ VRVerified Nothing where verify = verifyCmdAuthorization auth_ tAuth authorized dummyVerify = verify (dummyAuthKey tAuth) `seq` VRFailed @@ -597,6 +600,17 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv case command of SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody PING -> pure (corrId, "", PONG) + PRXY relay auth -> + ifM + allowProxy + (setupProxy relay) + (pure (corrId, queueId, ERR AUTH)) + where + allowProxy = do + ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config + pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth + PFWD _dhPub _encBlock -> error "TODO: processCommand.PFWD" + RFWD _encBlock -> error "TODO: processCommand.RFWD" Cmd SNotifier NSUB -> subscribeNotifications Cmd SRecipient command -> case command of @@ -922,6 +936,15 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Right q -> updateDeletedStats q $> ok Left e -> pure $ err e + setupProxy :: SMPServer -> M (Transmission BrokerMsg) + setupProxy todo'relay = undefined + -- do + -- let relaySessionId = "TODO: relaySessionId" + -- (dummyRelayDhPublic, _) <- atomically . C.generateKeyPair =<< asks random + -- (_, dummySignKey) <- atomically . C.generateKeyPair =<< asks random + -- let dummyRelayKeySignature = C.sign' dummySignKey $ smpEncode dummyRelayDhPublic + -- pure (corrId, relaySessionId, PKEY dummyRelayDhPublic dummyRelayKeySignature) + ok :: Transmission BrokerMsg ok = (corrId, queueId, OK) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index baadfc79b..74d7d96e3 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -15,12 +15,14 @@ import qualified Data.IntMap.Strict as IM import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Text (Text) import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) import Data.X509.Validation (Fingerprint (..)) import Network.Socket (ServiceName) import qualified Network.TLS as T import Numeric.Natural +import Simplex.Messaging.Agent.Env.SQLite (Worker) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Crypto (KeyHash (..)) import qualified Simplex.Messaging.Crypto as C @@ -33,7 +35,7 @@ import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, VersionSMP, VersionRangeSMP) +import Simplex.Messaging.Transport (ATransport, SessionId, VersionSMP, VersionRangeSMP) import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) @@ -79,7 +81,8 @@ data ServerConfig = ServerConfig -- | TCP transport config transportConfig :: TransportServerConfig, -- | run listener on control port - controlPort :: Maybe ServiceName + controlPort :: Maybe ServiceName, + allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth` } defMsgExpirationDays :: Int64 @@ -110,8 +113,9 @@ data Env = Env tlsServerParams :: T.ServerParams, serverStats :: ServerStats, sockets :: SocketState, - clientSeq :: TVar Int, - clients :: TVar (IntMap Client) + clientSeq :: TVar ClientId, + clients :: TVar (IntMap Client), + proxyServer :: SMPProxyServer -- senders served on this proxy } data Server = Server @@ -122,8 +126,22 @@ data Server = Server savingLock :: Lock } +data SMPProxyServer = SMPProxyServer + { relaySessions :: TMap SessionId SMPProxiedRelay, + relayServers :: TMap Text SessionId -- speed up client lookups by server URI + } + +data SMPProxiedRelay = SMPProxiedRelay + { worker :: Worker, + proxyKey :: C.DhSecretX25519, + fwdQ :: TBQueue (ClientId, CorrId, C.PublicKeyX25519, ByteString) -- FWD args from multiple clients using this server + -- can be used for QUOTA retries until the session is gone + } + +type ClientId = Int + data Client = Client - { clientId :: Int, + { clientId :: ClientId, subscriptions :: TMap RecipientId (TVar Sub), ntfSubscriptions :: TMap NotifierId (), rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)), @@ -135,7 +153,8 @@ data Client = Client connected :: TVar Bool, createdAt :: SystemTime, rcvActiveAt :: TVar SystemTime, - sndActiveAt :: TVar SystemTime + sndActiveAt :: TVar SystemTime, + proxyClient_ :: TVar (Maybe C.DhSecretX25519) -- this client is actually an SMP proxy } data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub @@ -154,7 +173,7 @@ newServer = do savingLock <- createLock return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock} -newClient :: TVar Int -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client +newClient :: TVar ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client newClient nextClientId qSize thVersion sessionId createdAt = do clientId <- stateTVar nextClientId $ \next -> (next, next + 1) subscriptions <- TM.empty @@ -166,7 +185,8 @@ newClient nextClientId qSize thVersion sessionId createdAt = do connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} + proxyClient_ <- newTVar Nothing + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, proxyClient_} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do @@ -187,7 +207,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, sockets <- atomically newSocketState clientSeq <- newTVarIO 0 clients <- newTVarIO mempty - return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients} + proxyServer <- newSMPProxyServer + return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyServer} where restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do @@ -203,3 +224,9 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, addNotifier q = case notifier q of Nothing -> id Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q) + +newSMPProxyServer :: MonadIO m => m SMPProxyServer +newSMPProxyServer = do + relayServers <- atomically TM.empty + relaySessions <- atomically TM.empty + pure SMPProxyServer {relayServers, relaySessions} diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index a7844cc95..d14bdac1f 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -213,7 +213,8 @@ smpServerCLI cfgPath logPath = defaultTransportServerConfig { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini }, - controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini + controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, + allowSMPProxy = True -- TODO: "get from INI" } data CliCommand diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6898af15d..3d4916b92 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -40,6 +40,7 @@ module Simplex.Messaging.Transport basicAuthSMPVersion, subModeSMPVersion, authCmdsSMPVersion, + sendingProxySMPVersion, simplexMQVersion, smpBlockSize, TransportConfig (..), @@ -148,6 +149,9 @@ subModeSMPVersion = VersionSMP 6 authCmdsSMPVersion :: VersionSMP authCmdsSMPVersion = VersionSMP 7 +sendingProxySMPVersion :: VersionSMP +sendingProxySMPVersion = VersionSMP 8 + currentClientSMPRelayVersion :: VersionSMP currentClientSMPRelayVersion = VersionSMP 6 diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 3e5e9d2ce..b63668f7a 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -73,7 +73,11 @@ testSMPClient = testSMPClientVR supportedClientSMPRelayVRange testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a testSMPClientVR vr client = do Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost - runTransportClient defaultTransportClientConfig Nothing useHost testPort (Just testKeyHash) $ \h -> do + testSMPClient_ useHost testPort vr client + +testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClient_ host port vr client = do + runTransportClient defaultTransportClientConfig Nothing host port (Just testKeyHash) $ \h -> do g <- C.newRandom ks <- atomically $ C.generateKeyPair g runExceptT (smpClientHandshake h ks testKeyHash vr) >>= \case @@ -107,12 +111,16 @@ cfg = certificateFile = "tests/fixtures/server.crt", smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig, - controlPort = Nothing + controlPort = Nothing, + allowSMPProxy = False } cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +proxyCfg :: ServerConfig +proxyCfg = cfg { allowSMPProxy = True } + withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs new file mode 100644 index 000000000..0a429fc57 --- /dev/null +++ b/tests/SMPProxyTests.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module SMPProxyTests where + +import SMPAgentClient (testSMPServer, testSMPServer2) +import SMPClient +import ServerTests (sendRecv) +import Simplex.Messaging.Protocol +import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) +import Simplex.Messaging.Transport +import Simplex.Messaging.Version (mkVersionRange) +import Test.Hspec +import Debug.Trace + +smpProxyTests :: Spec +smpProxyTests = do + describe "server configuration" $ do + it "refuses proxy handshake unless enabled" testNoProxy + it "checks basic auth in proxy requests" testProxyAuth + xdescribe "proxy requests" $ do + xdescribe "bad relay URIs" $ do + it "host not resolved" todo + it "when SMP port blackholed" todo + it "no SMP service at host/port" todo + it "bad SMP fingerprint" todo + it "connects to relay" testProxyConnect + xit "connects to itself as a relay" todo + xit "batching proxy requests" todo + xdescribe "forwarding requests" $ do + it "sender-proxy-relay-recipient works" todo + it "similar timing for proxied and direct sends" todo + +proxyVRange :: VersionRangeSMP +proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion + +testNoProxy :: IO () +testNoProxy = do + withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do + testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS) -> do + (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) + reply `shouldBe` Right (ERR AUTH) + +testProxyAuth :: IO () +testProxyAuth = do + withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + (_, s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") + traceShowM s + reply `shouldBe` Right (ERR AUTH) + where + proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} + +testProxyConnect :: IO () +testProxyConnect = do + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 Nothing) + case reply of + Right PKEY {} -> pure () + _ -> fail $ "bad reply: " <> show reply + +todo :: IO () +todo = do + fail "TODO" diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 09cf975c1..3b5f2f5dc 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -930,8 +930,6 @@ instance Eq C.ASignature where Just Refl -> s == s' _ -> False -deriving instance Eq (C.Signature a) - syntaxTests :: ATransport -> Spec syntaxTests (ATransport t) = do it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN) diff --git a/tests/Test.hs b/tests/Test.hs index aebceb22d..f9fb2a2c0 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -21,6 +21,7 @@ import GHC.IO.Exception (IOException (..)) import qualified GHC.IO.Exception as IOException import NtfServerTests (ntfServerTests) import RemoteControl (remoteControlTests) +import SMPProxyTests (smpProxyTests) import ServerTests import Simplex.Messaging.Transport (TLS, Transport (..)) import Simplex.Messaging.Transport.WebSockets (WS) @@ -59,6 +60,7 @@ main = do describe "SMP server via WebSockets" $ serverTests (transport @WS) describe "Notifications server" $ ntfServerTests (transport @TLS) describe "SMP client agent" $ agentTests (transport @TLS) + describe "SMP proxy" smpProxyTests describe "XFTP" $ do describe "XFTP server" xftpServerTests describe "XFTP file description" fileDescriptionTests From ad4b5b6b71cc2225cacdcdc75b799c2a6706d037 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 15 Apr 2024 13:47:48 +0100 Subject: [PATCH 002/125] parameterize transport handle with transport peer to include server certificate (#1100) * parameterize transport handle with transport peer to include server certificate * include server certificate into THandle * load server chain and sign key * fix key type * fix for 8.10 --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: IC Rainbow --- src/Simplex/FileTransfer/Client.hs | 13 ++-- src/Simplex/FileTransfer/Protocol.hs | 12 ++-- src/Simplex/FileTransfer/Server.hs | 16 ++--- src/Simplex/FileTransfer/Transport.hs | 9 +-- src/Simplex/Messaging/Client.hs | 14 ++-- src/Simplex/Messaging/Notifications/Server.hs | 10 +-- .../Messaging/Notifications/Server/Env.hs | 8 +-- .../Messaging/Notifications/Transport.hs | 44 +++++++----- src/Simplex/Messaging/Protocol.hs | 23 +++--- src/Simplex/Messaging/Server.hs | 18 ++--- src/Simplex/Messaging/Transport.hs | 70 ++++++++++++------- tests/CoreTests/BatchingTests.hs | 21 ++++-- tests/NtfClient.hs | 8 +-- tests/NtfServerTests.hs | 6 +- tests/SMPClient.hs | 36 +++++----- tests/SMPProxyTests.hs | 9 +-- tests/ServerTests.hs | 35 +++++----- 17 files changed, 198 insertions(+), 154 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index fcb54aece..a223d492e 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -51,7 +51,7 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client @@ -64,7 +64,7 @@ import UnliftIO.Directory data XFTPClient = XFTPClient { http2Client :: HTTP2Client, transportSession :: TransportSession FileResponse, - thParams :: THandleParams XFTPVersion, + thParams :: THandleParams XFTPVersion 'TClient, config :: XFTPClientConfig } @@ -120,19 +120,21 @@ getXFTPClient g transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN atomically $ writeTVar clientVar $ Just c pure c -xftpClientHandshakeV1 :: TVar ChaChaDRG -> VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP -> ExceptT XFTPClientError IO THandleParamsXFTP +xftpClientHandshakeV1 :: TVar ChaChaDRG -> VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient) xftpClientHandshakeV1 g serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do - shs <- getServerHandshake + shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake (v, sk) <- processServerHandshake shs (k, pk) <- atomically $ C.generateKeyPair g sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash, authPubKey = k} - pure thParams0 {thAuth = Just THandleAuth {peerPubKey = sk, privKey = pk}, thVersion = v} + pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, clientPrivKey = pk}, thVersion = v} where + getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake getServerHandshake = do let helloReq = H.requestNoBody "POST" "/" [] HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing liftHS . smpDecode =<< liftHS (C.unPad shsBody) + processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION case xftpVersionRange `compatibleVersion` serverVRange of @@ -145,6 +147,7 @@ xftpClientHandshakeV1 g serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessi _ -> throwError "bad certificate" pubKey <- maybe (throwError "bad server key type") (`C.verifyX509` exact) serverKey C.x509ToPublic (pubKey, []) >>= C.pubKey + sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO () sendClientHandshake chs = do chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' diff --git a/src/Simplex/FileTransfer/Protocol.hs b/src/Simplex/FileTransfer/Protocol.hs index f970fdfcc..418e48482 100644 --- a/src/Simplex/FileTransfer/Protocol.hs +++ b/src/Simplex/FileTransfer/Protocol.hs @@ -39,8 +39,8 @@ import Simplex.Messaging.Protocol ProtocolErrorType (..), ProtocolMsgTag (..), ProtocolType (..), - RcvPublicDhKey, RcvPublicAuthKey, + RcvPublicDhKey, RecipientId, SenderId, SentRawTransmission, @@ -48,14 +48,14 @@ import Simplex.Messaging.Protocol SndPublicAuthKey, Transmission, TransmissionForAuth (..), - encodeTransmissionForAuth, encodeTransmission, + encodeTransmissionForAuth, messageTagP, tDecodeParseValidate, tEncodeBatch1, tParse, ) -import Simplex.Messaging.Transport (THandleParams (..), TransportError (..)) +import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Util ((<$?>)) xftpBlockSize :: Int @@ -325,12 +325,12 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of Just Refl -> Just c _ -> Nothing -xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString +xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg) xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth -xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> Transmission c -> Either TransportError ByteString +xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString xftpEncodeTransmission thParams (corrId, fId, msg) = do let t = encodeTransmission thParams (corrId, fId, msg) xftpEncodeBatch1 (Nothing, t) @@ -339,7 +339,7 @@ xftpEncodeTransmission thParams (corrId, fId, msg) = do xftpEncodeBatch1 :: SentRawTransmission -> Either TransportError ByteString xftpEncodeBatch1 t = first (const TELargeMsg) $ C.pad (tEncodeBatch1 t) xftpBlockSize -xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> ByteString -> Either XFTPErrorType (SignedTransmission e c) +xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> ByteString -> Either XFTPErrorType (SignedTransmission e c) xftpDecodeTransmission thParams t = do t' <- first (const BLOCK) $ C.unPad t case tParse thParams t' of diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index b426360ea..b3bee134f 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -56,7 +56,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.Stats import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..)) +import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..)) import Simplex.Messaging.Transport.Buffer (trimCR) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize) @@ -75,7 +75,7 @@ import qualified UnliftIO.Exception as E type M a = ReaderT XFTPEnv IO a data XFTPTransportRequest = XFTPTransportRequest - { thParams :: THandleParamsXFTP, + { thParams :: THandleParamsXFTP 'TServer, reqBody :: HTTP2Body, request :: H.Request, sendResponse :: H.Response -> IO () @@ -91,7 +91,7 @@ runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpSer data Handshake = HandshakeSent C.PrivateKeyX25519 - | HandshakeAccepted THandleAuth VersionXFTP + | HandshakeAccepted (THandleAuth 'TServer) VersionXFTP xftpServer :: XFTPServerConfig -> TMVar Bool -> M () xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do @@ -120,7 +120,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira Nothing -> pure () -- handshake response sent Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here) _ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about - xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion)) + xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer)) xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do s <- atomically $ TM.lookup sessionId sessions r <- runExceptT $ case s of @@ -138,18 +138,18 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira shs <- encodeXftp hs liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing - processClientHandshake privKey = do + processClientHandshake pk = do unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE body <- liftHS $ C.unPad bodyHead XFTPClientHandshake {xftpVersion, keyHash, authPubKey} <- liftHS $ smpDecode body kh <- asks serverIdentity unless (keyHash == kh) $ throwError HANDSHAKE unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE - let auth = THandleAuth {peerPubKey = authPubKey, privKey} + let auth = THAuthServer {clientPeerPubKey = authPubKey, serverPrivKey = pk} atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions liftIO . sendResponse $ H.responseNoBody N.ok200 [] pure Nothing - sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion)) + sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) sendError err = do runExceptT (encodeXftp err) >>= \case Right bs -> liftIO . sendResponse $ H.responseBuilder N.ok200 [] bs @@ -326,7 +326,7 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea data VerificationResult = VRVerified XFTPRequest | VRFailed -verifyXFTPTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult +verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult verifyXFTPTransmission auth_ tAuth authorized fId cmd = case cmd of FileCmd SFSender (FNEW file rcps auth') -> pure $ XFTPReqNew file rcps auth' `verifyWith` sndKey file diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index d8bb72f6b..041069e98 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} @@ -51,7 +52,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol (CommandError) -import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..)) +import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.File import Simplex.Messaging.Util (bshow) import Simplex.Messaging.Version @@ -76,8 +77,8 @@ type VersionRangeXFTP = VersionRange XFTPVersion pattern VersionXFTP :: Word16 -> VersionXFTP pattern VersionXFTP v = Version v -type THandleXFTP c = THandle XFTPVersion c -type THandleParamsXFTP = THandleParams XFTPVersion +type THandleXFTP c p = THandle XFTPVersion c p +type THandleParamsXFTP p = THandleParams XFTPVersion p initialXFTPVersion :: VersionXFTP initialXFTPVersion = VersionXFTP 1 @@ -89,7 +90,7 @@ supportedFileServerVRange :: VersionRangeXFTP supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion -- XFTP protocol does not support handshake -xftpClientHandshakeStub :: c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c) +xftpClientHandshakeStub :: c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient) xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION data XFTPServerHandshake = XFTPServerHandshake diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index e0591b14d..61ec96003 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -84,8 +84,8 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception import Control.Monad -import Control.Monad.IO.Class (liftIO) import Control.Monad.Except +import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J @@ -119,7 +119,7 @@ import System.Timeout (timeout) -- Use 'getSMPClient' to connect to an SMP server and create a client handle. data ProtocolClient v err msg = ProtocolClient { action :: Maybe (Async ()), - thParams :: THandleParams v, + thParams :: THandleParams v 'TClient, sessionTs :: UTCTime, client_ :: PClient v err msg } @@ -138,7 +138,7 @@ data PClient v err msg = PClient msgQ :: Maybe (TBQueue (ServerTransmission v msg)) } -smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe THandleAuth -> STM SMPClient +smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient smpClientStub g sessionId thVersion thAuth = do connected <- newTVar False clientCorrId <- C.newRandomDRG g @@ -387,10 +387,10 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0]) `finally` disconnected c' - send :: Transport c => ProtocolClient v err msg -> THandle v c -> IO () + send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= tPutLog h - receive :: Transport c => ProtocolClient v err msg -> THandle v c -> IO () + receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () receive ProtocolClient {client_ = PClient {rcvQ}} h = forever $ tGet h >>= atomically . writeTBQueue rcvQ ping :: ProtocolClient v err msg -> IO () @@ -733,13 +733,13 @@ mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCo TM.insert corrId r sentCommands pure r -authTransmission :: Maybe THandleAuth -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) +authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_ where authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth authenticate (C.APrivateAuthKey a pk) = case a of C.SX25519 -> case thAuth of - Just THandleAuth {peerPubKey} -> Right $ TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t + Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t Nothing -> Left TENoServerAuth C.SEd25519 -> sign pk C.SEd448 -> sign pk diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 55ab40718..55bdb07eb 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -47,7 +47,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server import Simplex.Messaging.Server.Stats import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..)) +import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..)) import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials) import Simplex.Messaging.Util import System.Exit (exitFailure) @@ -339,7 +339,7 @@ updateTknStatus NtfTknData {ntfTknId, tknStatus} status = do old <- atomically $ stateTVar tknStatus (,status) when (old /= status) $ withNtfLog $ \sl -> logTokenStatus sl ntfTknId status -runNtfClientTransport :: Transport c => THandleNTF c -> M () +runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M () runNtfClientTransport th@THandle {params} = do qSize <- asks $ clientQSize . config ts <- liftIO getSystemTime @@ -356,7 +356,7 @@ runNtfClientTransport th@THandle {params} = do clientDisconnected :: NtfServerClient -> IO () clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connected False -receive :: Transport c => THandleNTF c -> NtfServerClient -> M () +receive :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> M () receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do ts <- liftIO $ tGet th forM_ ts $ \t@(_, _, (corrId, entId, cmdOrError)) -> do @@ -371,7 +371,7 @@ receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ where write q t = atomically $ writeTBQueue q t -send :: Transport c => THandleNTF c -> NtfServerClient -> IO () +send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO () send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do t <- atomically $ readTBQueue sndQ void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)] @@ -382,7 +382,7 @@ send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do data VerificationResult = VRVerified NtfRequest | VRFailed -verifyNtfTransmission :: Maybe (THandleAuth, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult +verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult verifyNtfTransmission auth_ (tAuth, authorized, (corrId, entId, _)) cmd = do st <- asks store case cmd of diff --git a/src/Simplex/Messaging/Notifications/Server/Env.hs b/src/Simplex/Messaging/Notifications/Server/Env.hs index 4a93a8a34..5bcd72f3d 100644 --- a/src/Simplex/Messaging/Notifications/Server/Env.hs +++ b/src/Simplex/Messaging/Notifications/Server/Env.hs @@ -24,16 +24,16 @@ import Numeric.Natural import Simplex.Messaging.Client.Agent import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol -import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Server.Stats import Simplex.Messaging.Notifications.Server.Store import Simplex.Messaging.Notifications.Server.StoreLog +import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF) import Simplex.Messaging.Protocol (CorrId, SMPServer, Transmission) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, THandleParams) +import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) @@ -161,13 +161,13 @@ data NtfRequest data NtfServerClient = NtfServerClient { rcvQ :: TBQueue NtfRequest, sndQ :: TBQueue (Transmission NtfResponse), - ntfThParams :: THandleParams NTFVersion, + ntfThParams :: THandleParams NTFVersion 'TServer, connected :: TVar Bool, rcvActiveAt :: TVar SystemTime, sndActiveAt :: TVar SystemTime } -newNtfServerClient :: Natural -> THandleParams NTFVersion -> SystemTime -> STM NtfServerClient +newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient newNtfServerClient qSize ntfThParams ts = do rcvQ <- newTBQueue qSize sndQ <- newTBQueue qSize diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index bc68fab03..d4a4a4cbe 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -5,6 +5,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} module Simplex.Messaging.Notifications.Transport where @@ -18,9 +19,9 @@ import qualified Data.X509 as X import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Transport +import Simplex.Messaging.Util (liftEitherWith) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal -import Simplex.Messaging.Util (liftEitherWith) ntfBlockSize :: Int ntfBlockSize = 512 @@ -54,7 +55,7 @@ supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVers supportedServerNTFVRange :: VersionRangeNTF supportedServerNTFVRange = mkVersionRange initialNTFVersion currentServerNTFVersion -type THandleNTF c = THandle NTFVersion c +type THandleNTF c p = THandle NTFVersion c p data NtfServerHandshake = NtfServerHandshake { ntfVersionRange :: VersionRangeNTF, @@ -111,7 +112,7 @@ encodeNtfAuthPubKey v k | otherwise = "" -- | Notifcations server transport handshake. -ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c) +ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TServer) ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c let sk = C.signX509 serverSignKey $ C.publicToX509 k @@ -121,11 +122,11 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do | keyHash /= kh -> throwError $ TEHandshake IDENTITY | v `isCompatible` ntfVRange -> - pure $ ntfThHandle th v pk k' + pure $ ntfThHandleServer th v pk k' | otherwise -> throwError $ TEHandshake VERSION -- | Notifcations server client transport handshake. -ntfClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c) +ntfClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) ntfClientHandshake c (k, pk) keyHash ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th @@ -133,23 +134,32 @@ ntfClientHandshake c (k, pk) keyHash ntfVRange = do then throwError TEBadSession else case ntfVersionRange `compatibleVersion` ntfVRange of Just (Compatible v) -> do - sk_ <- forM sk' $ \exact -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do + ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do serverKey <- getServerVerifyKey c - pubKey <- C.verifyX509 serverKey exact - C.x509ToPublic (pubKey, []) >>= C.pubKey + pubKey <- C.verifyX509 serverKey signedKey + (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash, authPubKey = Just k} - pure $ ntfThHandle th v pk sk_ + pure $ ntfThHandleClient th v pk ck_ Nothing -> throwError $ TEHandshake VERSION -ntfThHandle :: forall c. THandleNTF c -> VersionNTF -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleNTF c -ntfThHandle th@THandle {params} v privKey k_ = - -- TODO drop SMP v6: make thAuth non-optional - let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_ - v3 = v >= authBatchCmdsNTFVersion - params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} - in (th :: THandleNTF c) {params = params'} +ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleNTF c 'TServer +ntfThHandleServer th v pk k_ = + let thAuth = (\k -> THAuthServer {clientPeerPubKey = k, serverPrivKey = pk}) <$> k_ + in ntfThHandle_ th v thAuth -ntfTHandle :: Transport c => c -> THandleNTF c +ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient +ntfThHandleClient th v pk ck_ = + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, clientPrivKey = pk}) <$> ck_ + in ntfThHandle_ th v thAuth + +ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p +ntfThHandle_ th@THandle {params} v thAuth = + -- TODO drop SMP v6: make thAuth non-optional + let v3 = v >= authBatchCmdsNTFVersion + params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} + in (th :: THandleNTF c p) {params = params'} + +ntfTHandle :: Transport c => c -> THandleNTF c p ntfTHandle c = THandle {connection = c, params} where params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False} diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 77b14e23c..b5a3e7770 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -1135,7 +1135,7 @@ data CommandError deriving (Eq, Read, Show) -- | SMP transmission parser. -transmissionP :: THandleParams v -> Parser RawTransmission +transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do authenticator <- smpP authorized <- A.takeByteString @@ -1149,10 +1149,10 @@ transmissionP THandleParams {sessionId, implySessId} = do command <- A.takeByteString pure RawTransmission {authenticator, authorized = authorized', sessId, corrId, entityId, command} -class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where +class (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 :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c) + protocolClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c 'TClient) protocolPing :: ProtoCommand msg protocolError :: msg -> Maybe err @@ -1258,7 +1258,6 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where PFWD_ -> error "TODO: PFWD_" RFWD_ -> error "TODO: RFWD_" PRXY_ -> PRXY <$> (_smpP >>= either fail pure . strDecode) <*> _smpP - CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1386,7 +1385,7 @@ instance Encoding CommandError where _ -> fail "bad command error type" -- | Send signed SMP transmission to TCP transport. -tPut :: Transport c => THandle v c -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] +tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (batch params) (blockSize params) where tPutBatch :: TransportBatch () -> IO [Either TransportError ()] @@ -1395,7 +1394,7 @@ tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (ba TBTransmissions s n _ -> replicate n <$> tPutLog th s TBTransmission s _ -> (: []) <$> tPutLog th s -tPutLog :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ()) +tPutLog :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutLog th s = do r <- tPutBlock th s case r of @@ -1461,7 +1460,7 @@ tEncodeBatch1 t = lenEncode 1 `B.cons` tEncodeForBatch t -- 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 -> Transmission c -> TransmissionForAuth +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 @@ -1469,7 +1468,7 @@ encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t' = encodeTransmission_ v t {-# INLINE encodeTransmissionForAuth #-} -encodeTransmission :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> ByteString +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 @@ -1482,11 +1481,11 @@ encodeTransmission_ v (CorrId corrId, queueId, command) = {-# INLINE encodeTransmission_ #-} -- | Receive and parse transmission from the TCP transport (ignoring any trailing padding). -tGetParse :: Transport c => THandle v c -> IO (NonEmpty (Either TransportError RawTransmission)) +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 -> ByteString -> NonEmpty (Either TransportError RawTransmission) +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] @@ -1498,10 +1497,10 @@ eitherList :: (a -> NonEmpty (Either e b)) -> Either e a -> NonEmpty (Either e b eitherList = either (\e -> [Left e]) -- | Receive client and server transmissions (determined by `cmd` type). -tGet :: forall v err cmd c. (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (NonEmpty (SignedTransmission err cmd)) +tGet :: forall v err cmd c p. (ProtocolEncoding v err cmd, Transport c) => THandle v c p -> IO (NonEmpty (SignedTransmission err cmd)) tGet th@THandle {params} = L.map (tDecodeParseValidate params) <$> tGetParse th -tDecodeParseValidate :: forall v err cmd. ProtocolEncoding v err cmd => THandleParams v -> Either TransportError RawTransmission -> SignedTransmission err cmd +tDecodeParseValidate :: forall v p err cmd. ProtocolEncoding v err cmd => THandleParams v p -> Either TransportError RawTransmission -> SignedTransmission err cmd tDecodeParseValidate THandleParams {sessionId, thVersion = v, implySessId} = \case Right RawTransmission {authenticator, authorized, sessId, corrId, entityId, command} | implySessId || sessId == sessionId -> diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 456de5be2..2ff0d5e51 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -409,7 +409,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do logError "Unauthorized control port command" hPutStrLn h "AUTH" -runClientTransport :: Transport c => THandleSMP c -> M () +runClientTransport :: Transport c => THandleSMP c 'TServer -> M () runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do q <- asks $ tbqSize . config ts <- liftIO getSystemTime @@ -457,7 +457,7 @@ cancelSub sub = Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread _ -> return () -receive :: Transport c => THandleSMP c -> Client -> M () +receive :: Transport c => THandleSMP c 'TServer -> Client -> M () receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive" forever $ do @@ -478,7 +478,7 @@ receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActi VRFailed -> Left (corrId, queueId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty -send :: Transport c => THandleSMP c -> Client -> IO () +send :: Transport c => THandleSMP c 'TServer -> Client -> IO () send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send" forever $ do @@ -493,7 +493,7 @@ send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do NMSG {} -> 0 _ -> 1 -disconnectTransport :: Transport c => THandle v c -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () +disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " disconnectTransport" loop @@ -514,7 +514,7 @@ data VerificationResult = VRVerified (Maybe QueueRec) | VRFailed -- - the queue or party key do not exist. -- In all cases, the time of the verification should depend only on the provided authorization type, -- a dummy key is used to run verification in the last two cases, and failure is returned irrespective of the result. -verifyTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult +verifyTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult verifyTransmission auth_ tAuth authorized queueId cmd = case cmd of Cmd SRecipient (NEW k _ _ _) -> pure $ Nothing `verifiedWith` k @@ -539,7 +539,7 @@ verifyTransmission auth_ tAuth authorized queueId cmd = st <- asks queueStore atomically $ getQueue st party queueId -verifyCmdAuthorization :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool +verifyCmdAuthorization :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAuth where verify :: C.APublicAuthKey -> TransmissionAuth -> Bool @@ -551,12 +551,12 @@ verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAu C.SX25519 -> verifyCmdAuth auth_ k s authorized _ -> verifyCmdAuth auth_ dummyKeyX25519 s authorized `seq` False -verifyCmdAuth :: Maybe (THandleAuth, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool +verifyCmdAuth :: Maybe (THandleAuth 'TServer, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool verifyCmdAuth auth_ k authenticator authorized = case auth_ of - Just (THandleAuth {privKey}, nonce) -> C.cbVerify k privKey nonce authenticator authorized + Just (THAuthServer {serverPrivKey = pk}, nonce) -> C.cbVerify k pk nonce authenticator authorized Nothing -> False -dummyVerifyCmd :: Maybe (THandleAuth, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool +dummyVerifyCmd :: Maybe (THandleAuth 'TServer, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool dummyVerifyCmd auth_ authorized = \case TASignature (C.ASignature a s) -> C.verify' (dummySignKey a) s authorized TAAuthenticator s -> verifyCmdAuth auth_ dummyKeyX25519 s authorized diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 3d4916b92..565199c36 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -12,6 +12,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} -- | @@ -315,20 +316,20 @@ instance Transport TLS where -- * SMP transport -- | The handle for SMP encrypted transport connection over Transport. -data THandle v c = THandle +data THandle v c p = THandle { connection :: c, - params :: THandleParams v + params :: THandleParams v p } -type THandleSMP c = THandle SMPVersion c +type THandleSMP c p = THandle SMPVersion c p -data THandleParams v = THandleParams +data THandleParams v p = THandleParams { sessionId :: SessionId, blockSize :: Int, -- | agreed server protocol version thVersion :: Version v, -- | peer public key for command authorization and shared secrets for entity ID encryption - thAuth :: Maybe THandleAuth, + thAuth :: Maybe (THandleAuth p), -- | do NOT send session ID in transmission, but include it into signed message -- based on protocol version implySessId :: Bool, @@ -337,10 +338,18 @@ data THandleParams v = THandleParams batch :: Bool } -data THandleAuth = THandleAuth - { peerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key - privKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) - } +data THandleAuth (p :: TransportPeer) where + THAuthClient :: + { serverPeerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key + serverCertKey :: (X.CertificateChain, X.SignedExact X.PubKey), -- the key here is clientPrivKey signed with server certificate + clientPrivKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) + } -> + THandleAuth 'TClient + THAuthServer :: + { clientPeerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key + serverPrivKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) + } -> + THandleAuth 'TServer -- | TLS-unique channel binding type SessionId = ByteString @@ -442,13 +451,13 @@ serializeTransportError = \case TEHandshake e -> "HANDSHAKE " <> bshow e -- | Pad and send block to SMP transport. -tPutBlock :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ()) +tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutBlock THandle {connection = c, params = THandleParams {blockSize}} block = bimapM (const $ pure TELargeMsg) (cPut c) $ C.pad block blockSize -- | Receive block from SMP transport. -tGetBlock :: Transport c => THandle v c -> IO (Either TransportError ByteString) +tGetBlock :: Transport c => THandle v c p -> IO (Either TransportError ByteString) tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do msg <- cGet c blockSize if B.length msg == blockSize @@ -458,7 +467,7 @@ tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do -- | Server SMP transport handshake. -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c) +smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TServer) smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c sk = C.signX509 serverSignKey $ C.publicToX509 k @@ -469,13 +478,13 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do | keyHash /= kh -> throwE $ TEHandshake IDENTITY | v `isCompatible` smpVRange -> - pure $ smpThHandle th v pk k' + pure $ smpThHandleServer th v pk k' | otherwise -> throwE $ TEHandshake VERSION -- | Client SMP transport handshake. -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -smpClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c) +smpClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TClient) smpClientHandshake c (k, pk) keyHash@(C.KeyHash kh) smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th @@ -483,33 +492,42 @@ smpClientHandshake c (k, pk) keyHash@(C.KeyHash kh) smpVRange = do then throwE TEBadSession else case smpVersionRange `compatibleVersion` smpVRange of Just (Compatible v) -> do - sk_ <- forM authPubKey $ \(X.CertificateChain cert, exact) -> + ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () _ -> throwError "bad certificate" serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey exact - C.x509ToPublic (pubKey, []) >>= C.pubKey + (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = Just k} - pure $ smpThHandle th v pk sk_ + pure $ smpThHandleClient th v pk ck_ Nothing -> throwE $ TEHandshake VERSION -smpThHandle :: forall c. THandleSMP c -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c -smpThHandle th@THandle {params} v privKey k_ = - -- TODO drop SMP v6: make thAuth non-optional - let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_ - params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} - in (th :: THandleSMP c) {params = params'} +smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpThHandleServer th v pk k_ = + let thAuth = (\k -> THAuthServer {clientPeerPubKey = k, serverPrivKey = pk}) <$> k_ + in smpThHandle_ th v thAuth -sendHandshake :: (Transport c, Encoding smp) => THandle v c -> smp -> ExceptT TransportError IO () +smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpThHandleClient th v pk ck_ = + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, clientPrivKey = pk}) <$> ck_ + in smpThHandle_ th v thAuth + +smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpThHandle_ th@THandle {params} v thAuth = + -- TODO drop SMP v6: make thAuth non-optional + let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} + in (th :: THandleSMP c p) {params = params'} + +sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () sendHandshake th = ExceptT . tPutBlock th . smpEncode -- ignores tail bytes to allow future extensions -getHandshake :: (Transport c, Encoding smp) => THandle v c -> ExceptT TransportError IO smp +getHandshake :: (Transport c, Encoding smp) => THandle v c p -> ExceptT TransportError IO smp getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP =<<) <$> tGetBlock th -smpTHandle :: Transport c => c -> THandleSMP c +smpTHandle :: Transport c => c -> THandleSMP c p smpTHandle c = THandle {connection = c, params} where params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True} diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index 996d2fed1..c349096dc 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} module CoreTests.BatchingTests (batchingTests) where @@ -11,6 +13,9 @@ import Crypto.Random (ChaChaDRG) import qualified Data.ByteString as B import Data.ByteString.Char8 (ByteString) import qualified Data.List.NonEmpty as L +import qualified Data.X509 as X +import qualified Data.X509.CertificateStore as XS +import qualified Data.X509.File as XF import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol @@ -314,7 +319,7 @@ randomSEND_ a v sessId len = do TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, sId, Cmd SSender $ SEND noMsgFlags msg) pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) corrId tForAuth -testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion +testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient testTHandleParams v sessionId = THandleParams { sessionId, @@ -325,11 +330,17 @@ testTHandleParams v sessionId = batch = True } -testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe THandleAuth) -testTHandleAuth v g (C.APublicAuthKey a k) = case a of +testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe (THandleAuth 'TClient)) +testTHandleAuth v g (C.APublicAuthKey a serverPeerPubKey) = case a of C.SX25519 | v >= authCmdsSMPVersion -> do - (_, privKey) <- atomically $ C.generateKeyPair g - pure $ Just THandleAuth {peerPubKey = k, privKey} + (_, clientPrivKey) <- atomically $ C.generateKeyPair @'C.X25519 g + ca <- head <$> XS.readCertificates "tests/fixtures/ca.crt" + serverCert <- head <$> XS.readCertificates "tests/fixtures/server.crt" + serverKey <- head <$> XF.readKeyFile "tests/fixtures/server.key" + signKey <- either error pure $ C.x509ToPrivate (serverKey, []) >>= C.privKey @C.APrivateSignKey + (serverAuthPub, _) <- atomically $ C.generateKeyPair @'C.X25519 g + let serverCertKey = (X.CertificateChain [serverCert, ca], C.signX509 signKey $ C.toPubKey C.publicToX509 serverAuthPub) + pure $ Just THAuthClient {serverPeerPubKey, serverCertKey, clientPrivKey} _ -> pure Nothing randomSENDCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> Int -> IO (PCTransmission ErrorType BrokerMsg) diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index 26981628f..d47722944 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -70,7 +70,7 @@ testKeyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=" ntfTestStoreLogFile :: FilePath ntfTestStoreLogFile = "tests/tmp/ntf-server-store.log" -testNtfClient :: Transport c => (THandleNTF c -> IO a) -> IO a +testNtfClient :: Transport c => (THandleNTF c 'TClient -> IO a) -> IO a testNtfClient client = do Right host <- pure $ chooseTransportHost defaultNetworkConfig testHost runTransportClient defaultTransportClientConfig Nothing host ntfTestPort (Just testKeyHash) $ \h -> do @@ -139,7 +139,7 @@ withNtfServerOn t port' = withNtfServerThreadOn t port' . const withNtfServer :: ATransport -> IO a -> IO a withNtfServer t = withNtfServerOn t ntfTestPort -runNtfTest :: forall c a. Transport c => (THandleNTF c -> IO a) -> IO a +runNtfTest :: forall c a. Transport c => (THandleNTF c 'TClient -> IO a) -> IO a runNtfTest test = withNtfServer (transport @c) $ testNtfClient test ntfServerTest :: @@ -150,7 +150,7 @@ ntfServerTest :: IO (Maybe TransmissionAuth, ByteString, ByteString, NtfResponse) ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h where - tPut' :: THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () + tPut' :: THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp) [Right ()] <- tPut h [Right (sig, t')] @@ -159,7 +159,7 @@ ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h [(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h pure (Nothing, corrId, qId, cmd) -ntfTest :: Transport c => TProxy c -> (THandleNTF c -> IO ()) -> Expectation +ntfTest :: Transport c => TProxy c -> (THandleNTF c 'TClient -> IO ()) -> Expectation ntfTest _ test' = runNtfTest test' `shouldReturn` () data APNSMockRequest = APNSMockRequest diff --git a/tests/NtfServerTests.hs b/tests/NtfServerTests.hs index e7e2018c2..e5f096eef 100644 --- a/tests/NtfServerTests.hs +++ b/tests/NtfServerTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module NtfServerTests where @@ -72,13 +72,13 @@ pattern RespNtf corrId queueId command <- (_, _, (corrId, queueId, Right command deriving instance Eq NtfResponse -sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) +sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) sendRecvNtf h@THandle {params} (sgn, corrId, qId, cmd) = do let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (sgn, tToSend) tGet1 h -signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) +signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) signSendRecvNtf h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (authorize tForAuth, tToSend) diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index b63668f7a..cbdf4319d 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -67,15 +67,15 @@ xit'' d t = do ci <- runIO $ lookupEnv "CI" (if ci == Just "true" then skip "skipped on CI" . it d else it d) t -testSMPClient :: Transport c => (THandleSMP c -> IO a) -> IO a +testSMPClient :: Transport c => (THandleSMP c 'TClient -> IO a) -> IO a testSMPClient = testSMPClientVR supportedClientSMPRelayVRange -testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a testSMPClientVR vr client = do Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost testSMPClient_ useHost testPort vr client -testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a testSMPClient_ host port vr client = do runTransportClient defaultTransportClientConfig Nothing host port (Just testKeyHash) $ \h -> do g <- C.newRandom @@ -119,7 +119,7 @@ cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} proxyCfg :: ServerConfig -proxyCfg = cfg { allowSMPProxy = True } +proxyCfg = cfg {allowSMPProxy = True} withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} @@ -158,16 +158,16 @@ withSmpServer t = withSmpServerOn t testPort withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const -runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c -> IO a) -> IO a +runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a runSmpTest test = withSmpServer (transport @c) $ testSMPClient test -runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a +runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a runSmpTestN = runSmpTestNCfg cfg supportedClientSMPRelayVRange -runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a +runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a runSmpTestNCfg srvCfg clntVR nClients test = withSmpServerConfigOn (transport @c) srvCfg testPort $ \_ -> run nClients [] where - run :: Int -> [THandleSMP c] -> IO a + run :: Int -> [THandleSMP c 'TClient] -> IO a run 0 hs = test hs run n hs = testSMPClientVR clntVR $ \h -> run (n - 1) (h : hs) @@ -179,7 +179,7 @@ smpServerTest :: IO (Maybe TransmissionAuth, ByteString, ByteString, BrokerMsg) smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h where - tPut' :: THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () + tPut' :: THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp) [Right ()] <- tPut h [Right (sig, t')] @@ -188,33 +188,33 @@ smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h [(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h pure (Nothing, corrId, qId, cmd) -smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> IO ()) -> Expectation +smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> IO ()) -> Expectation smpTest _ test' = runSmpTest test' `shouldReturn` () -smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO ()) -> Expectation +smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO ()) -> Expectation smpTestN n test' = runSmpTestN n test' `shouldReturn` () -smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest2 = smpTest2Cfg cfg supportedClientSMPRelayVRange -smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest2Cfg srvCfg clntVR _ test' = runSmpTestNCfg srvCfg clntVR 2 _test `shouldReturn` () where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2] = test' h1 h2 _test _ = error "expected 2 handles" -smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest3 _ test' = smpTestN 3 _test where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2, h3] = test' h1 h2 h3 _test _ = error "expected 3 handles" -smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest4 _ test' = smpTestN 4 _test where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2, h3, h4] = test' h1 h2 h3 h4 _test _ = error "expected 4 handles" diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 0a429fc57..6cdf1f590 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -5,6 +6,7 @@ module SMPProxyTests where +import Debug.Trace import SMPAgentClient (testSMPServer, testSMPServer2) import SMPClient import ServerTests (sendRecv) @@ -13,7 +15,6 @@ import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec -import Debug.Trace smpProxyTests :: Spec smpProxyTests = do @@ -39,14 +40,14 @@ proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do - testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS) -> do + testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) reply `shouldBe` Right (ERR AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") traceShowM s reply `shouldBe` Right (ERR AUTH) @@ -56,7 +57,7 @@ testProxyAuth = do testProxyConnect :: IO () testProxyConnect = do withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS) -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 Nothing) case reply of Right PKEY {} -> pure () diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 3b5f2f5dc..a7e8fee86 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -78,13 +78,13 @@ pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh) pattern Msg :: MsgId -> MsgBody -> BrokerMsg pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} -sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) +sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) sendRecv h@THandle {params} (sgn, corrId, qId, cmd) = do let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (sgn, tToSend) tGet1 h -signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) +signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (authorize tForAuth, tToSend) @@ -93,17 +93,17 @@ signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do authorize t = case a of C.SEd25519 -> Just . TASignature . C.ASignature C.SEd25519 $ C.sign' pk t C.SEd448 -> Just . TASignature . C.ASignature C.SEd448 $ C.sign' pk t - C.SX25519 -> (\THandleAuth {peerPubKey} -> TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t) <$> thAuth params + C.SX25519 -> (\THAuthClient {serverPeerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t) <$> thAuth params #if !MIN_VERSION_base(4,18,0) _sx448 -> undefined -- ghc8107 fails to the branch excluded by types #endif -tPut1 :: Transport c => THandle v c -> SentRawTransmission -> IO (Either TransportError ()) +tPut1 :: Transport c => THandle v c 'TClient -> SentRawTransmission -> IO (Either TransportError ()) tPut1 h t = do [r] <- tPut h [Right t] pure r -tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (SignedTransmission err cmd) +tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c 'TClient -> IO (SignedTransmission err cmd) tGet1 h = do [r] <- liftIO $ tGet h pure r @@ -555,12 +555,12 @@ testWithStoreLog at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 removeFile testStoreLogFile where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () logSize :: FilePath -> IO Int @@ -653,12 +653,12 @@ testRestoreMessages at@(ATransport t) = removeFile testStoreMsgsFile removeFile testServerStatsBackupFile where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () checkStats :: ServerStatsData -> [RecipientId] -> Int -> Int -> Expectation @@ -727,15 +727,15 @@ testRestoreExpireMessages at@(ATransport t) = Right ServerStatsData {_msgExpired} <- strDecode <$> B.readFile testServerStatsBackupFile _msgExpired `shouldBe` 2 where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () -createAndSecureQueue :: Transport c => THandleSMP c -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret) +createAndSecureQueue :: Transport c => THandleSMP c 'TClient -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret) createAndSecureQueue h sPub = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g @@ -759,8 +759,8 @@ testTiming (ATransport t) = timingTests :: [(C.AuthAlg, C.AuthAlg, Int)] timingTests = [ (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd25519, 200), -- correct key type - -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150), - -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200), + -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150), + -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200), (C.AuthAlg C.SEd448, C.AuthAlg C.SEd25519, 200), (C.AuthAlg C.SEd448, C.AuthAlg C.SEd448, 150), -- correct key type (C.AuthAlg C.SEd448, C.AuthAlg C.SX25519, 200), @@ -770,7 +770,7 @@ testTiming (ATransport t) = ] timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const similarTime t1 t2 = abs (t2 / t1 - 1) < 0.2 -- normally the difference between "no queue" and "wrong key" is less than 5% - testSameTiming :: forall c. Transport c => THandleSMP c -> THandleSMP c -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation + testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, n) = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g @@ -791,10 +791,11 @@ testTiming (ATransport t) = runTimingTest sh badKey sId $ _SEND "hello" where - runTimingTest :: PartyI p => THandleSMP c -> C.APrivateAuthKey -> ByteString -> Command p -> IO () + runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> ByteString -> Command p -> IO () runTimingTest h badKey qId cmd = do threadDelay 100000 - _ <- timeRepeat n $ do -- "warm up" the server + _ <- timeRepeat n $ do + -- "warm up" the server Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd) return () threadDelay 100000 From a3b229f668bc39da24b206c3e7f2f0e567717498 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 18 Apr 2024 22:35:17 +0100 Subject: [PATCH 003/125] SMP proxy: low level client and server implementation (#1096) * SMP proxy: low level client and server implementation * SMP proxy: server implementation (#1098) * wip * PRXY command * progress * SMP Proxy: client-level implementation (#1101) * buildable * encode messages * update pkey * fix queue types * wrap SEND in proxy lookup * WIP proxy client * WIP * post-rebase fixes * encode something with something * cleanup * update * fix nonce/corrId in batchingTests * WIP: dig into createSMPProxySession * agent * test progress * pass the test * parameterize transport handle with transport peer to include server certificate (#1100) * parameterize transport handle with transport peer to include server certificate * include server certificate into THandle * load server chain and sign key * fix key type * fix for 8.10 --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: IC Rainbow * cleanup * add 2-server test * remove subsumed test * checkCredentials for BrokerMsg * skip batching tests * remove userId param * remove agent changes --------- Co-authored-by: Evgeny Poberezkin --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> * remove unused type * icrease test timeout * reduce transport block * envelope sizes * don't fork unless have proxied commands to process --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: IC Rainbow --- .github/workflows/build.yml | 2 +- src/Simplex/FileTransfer/Client.hs | 4 +- src/Simplex/FileTransfer/Protocol.hs | 3 +- src/Simplex/Messaging/Agent.hs | 2 +- src/Simplex/Messaging/Client.hs | 149 ++++++++++++-- src/Simplex/Messaging/Client/Agent.hs | 21 +- src/Simplex/Messaging/Crypto.hs | 4 + src/Simplex/Messaging/Protocol.hs | 185 ++++++++++++++---- src/Simplex/Messaging/Server.hs | 185 +++++++++++++----- src/Simplex/Messaging/Server/Env/STM.hs | 35 ++-- src/Simplex/Messaging/Server/Main.hs | 7 +- .../Messaging/Server/QueueStore/STM.hs | 2 +- src/Simplex/Messaging/Transport.hs | 6 + tests/CoreTests/BatchingTests.hs | 12 +- tests/CoreTests/ProtocolErrorTests.hs | 10 +- tests/SMPClient.hs | 11 +- tests/SMPProxyTests.hs | 117 ++++++++--- tests/Test.hs | 2 +- 18 files changed, 583 insertions(+), 174 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4c44cff9..4a61490ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: run: cabal build --enable-tests - name: Test - timeout-minutes: 30 + timeout-minutes: 40 shell: bash run: cabal test --test-show-details=direct diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index a223d492e..2606d1bb9 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -186,9 +186,11 @@ xftpClientError = \case sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body) sendXFTPCommand c@XFTPClient {thParams} pKey fId cmd chunkSpec_ = do + -- TODO random corrId + let corrIdUsedAsNonce = "" t <- liftEither . first PCETransportError $ - xftpEncodeAuthTransmission thParams pKey ("", fId, FileCmd (sFileParty @p) cmd) + xftpEncodeAuthTransmission thParams pKey (corrIdUsedAsNonce, fId, FileCmd (sFileParty @p) cmd) sendXFTPTransmission c t chunkSpec_ sendXFTPTransmission :: XFTPClient -> ByteString -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body) diff --git a/src/Simplex/FileTransfer/Protocol.hs b/src/Simplex/FileTransfer/Protocol.hs index 418e48482..c55b327f8 100644 --- a/src/Simplex/FileTransfer/Protocol.hs +++ b/src/Simplex/FileTransfer/Protocol.hs @@ -48,6 +48,7 @@ import Simplex.Messaging.Protocol SndPublicAuthKey, Transmission, TransmissionForAuth (..), + CorrId (..), encodeTransmission, encodeTransmissionForAuth, messageTagP, @@ -328,7 +329,7 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg) - xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth + xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) (C.cbNonce $ bs corrId) tForAuth xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString xftpEncodeTransmission thParams (corrId, fId, msg) = do diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index e0cfb2aca..2d9fedc65 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -788,7 +788,7 @@ compatibleContactUri (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = AgentConfig {smpClientVRange, smpAgentVRange} <- asks config pure $ (,) - <$> (qUri `compatibleVersion` smpClientVRange) + <$> (qUri `compatibleVersion` smpClientVRange) <*> (crAgentVRange `compatibleVersion` smpAgentVRange pqSup) versionPQSupport_ :: VersionSMPA -> Maybe CR.VersionE2E -> PQSupport diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 61ec96003..1c7196495 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -54,6 +54,9 @@ module Simplex.Messaging.Client suspendSMPQueue, deleteSMPQueue, deleteSMPQueues, + createSMPProxySession, + proxySMPMessage, + forwardSMPMessage, sendProtocolCommand, -- * Supporting types and client configuration @@ -69,6 +72,7 @@ module Simplex.Messaging.Client chooseTransportHost, proxyUsername, temporaryClientError, + smpProxyError, ServerTransmission, ClientCommand, @@ -98,9 +102,12 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Time.Clock (UTCTime (..), getCurrentTime) +import qualified Data.X509 as X +import qualified Data.X509.Validation as XV import Network.Socket (ServiceName) import Numeric.Natural import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Protocol @@ -110,7 +117,7 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), runTransportClient) import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Transport.WebSockets (WS) -import Simplex.Messaging.Util (bshow, raceAny_, threadDelay') +import Simplex.Messaging.Util (bshow, liftEitherWith, raceAny_, threadDelay') import Simplex.Messaging.Version import System.Timeout (timeout) @@ -480,6 +487,19 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} +-- TODO keep error params +smpProxyError :: SMPClientError -> ErrorType +smpProxyError = \case + PCEProtocolError _ -> PROXY PROTOCOL + PCEResponseError _ -> PROXY RESPONSE + PCEUnexpectedResponse _ -> PROXY UNEXPECTED + PCEResponseTimeout -> PROXY TIMEOUT + PCENetworkError -> PROXY NETWORK + PCEIncompatibleHost -> PROXY BAD_HOST + PCETransportError _ -> PROXY TRANSPORT + PCECryptoError _ -> INTERNAL + PCEIOError _ -> INTERNAL + -- | Create a new SMP queue. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command @@ -630,6 +650,102 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO deleteSMPQueues = okSMPCommands DEL {-# INLINE deleteSMPQueues #-} +-- TODO picture + +-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender +-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg +createSMPProxySession :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO (SessionId, VersionSMP, C.PublicKeyX25519) +createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = + sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case + -- XXX: rfc says sessionId should be in the entityId of response + PKEY sId vr (chain, key) -> do + case supportedClientSMPRelayVRange `compatibleVersion` vr of + Nothing -> throwE PCEIncompatibleHost -- TODO different error + Just (Compatible v) -> liftEitherWith x509Error $ (sId,v,) <$> validateRelay chain key + r -> throwE . PCEUnexpectedResponse $ bshow r + where + x509Error :: String -> SMPClientError + x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error + validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 + validateRelay (X.CertificateChain cert) exact = do + serverKey <- case cert of + [leaf, ca] + | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> + C.x509ToPublic (X.certPubKey . X.signedObject $ X.getSigned leaf, []) >>= C.pubKey + _ -> throwError "bad certificate" + pubKey <- C.verifyX509 serverKey exact + C.x509ToPublic (pubKey, []) >>= C.pubKey + +-- consider how to process slow responses - is it handled somehow locally or delegated to the caller +-- this method is used in the client +-- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender +-- receives PRES :: EncResponse -> BrokerMsg -- proxy to client +proxySMPMessage :: + SMPClient -> + -- proxy session from PKEY + SessionId -> + VersionSMP -> + C.PublicKeyX25519 -> + -- message to deliver + Maybe SndPrivateAuthKey -> + SenderId -> + MsgFlags -> + MsgBody -> + ExceptT SMPClientError IO () +-- TODO use version +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} sessionId _v serverKey spKey sId flags msg = do + -- prepare params + let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams + serverThParams = proxyThParams {sessionId, thAuth = serverThAuth} + (cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g + let cmdSecret = C.dh' serverKey cmdPrivKey + nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g + -- encode + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) + auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth + b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of + [] -> throwE $ PCETransportError TELargeMsg -- some other error. Internal? + TBError e _ : _ -> throwE $ PCETransportError e -- large message error? + TBTransmission s _ : _ -> pure s + TBTransmissions s _ _ : _ -> pure s + et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength + sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case + -- TODO support PKEY + resend? + PRES (EncResponse er) -> do + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + case tParse proxyThParams t' of + t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + (_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify + Left e -> throwE $ PCEResponseError e + Right OK -> pure () + Right (ERR e) -> throwE $ PCEProtocolError e + Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy + _ -> throwE $ PCETransportError TEBadBlock + r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy + +-- this method is used in the proxy +-- sends RFWD :: EncFwdTransmission -> Command Sender +-- receives RRES :: EncFwdResponse -> BrokerMsg +-- proxy should send PRES to the client with EncResponse +forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do + -- prepare params + sessSecret <- case thAuth thParams of + Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key? + Just THAuthClient {serverPeerPubKey, clientPrivKey} -> pure $ C.dh' serverPeerPubKey clientPrivKey + nonce <- liftIO . atomically $ C.randomCbNonce g + -- wrap + let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} + eft <- liftEitherWith PCECryptoError $ EncFwdTransmission <$> C.cbEncrypt sessSecret nonce (smpEncode fwdT) paddedForwardedMsgLength + -- send + sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case + RRES (EncFwdResponse efr) -> do + -- unwrap + r' <- liftEitherWith PCECryptoError $ C.cbDecrypt sessSecret (C.reverseNonce nonce) efr + FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' + pure fwdResponse + r -> throwE . PCEUnexpectedResponse $ bshow r + okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case @@ -693,8 +809,11 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do -- | Send Protocol command sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} pKey entId cmd = - ExceptT $ uncurry sendRecv =<< mkTransmission c (pKey, entId, cmd) +sendProtocolCommand c = sendProtocolCommand_ c Nothing + +sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ pKey entId cmd = + ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd) where -- two separate "atomically" needed to avoid blocking sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg) @@ -713,33 +832,35 @@ getResponse :: ProtocolClient v err msg -> Request err msg -> IO (Response err m getResponse ProtocolClient {client_ = PClient {tcpTimeout, pingErrorCount}} Request {entityId, responseVar} = do response <- timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case + -- BTW: another registerDelay candidate. Also, crashes caller with BlockedIndef. Just r -> atomically (writeTVar pingErrorCount 0) $> r Nothing -> pure $ Left PCEResponseTimeout pure Response {entityId, response} -mkTransmission :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) -mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} (pKey_, entId, cmd) = do - corrId <- atomically getNextCorrId - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, entId, cmd) - auth = authTransmission (thAuth thParams) pKey_ corrId tForAuth - r <- atomically $ mkRequest corrId +mkTransmission :: ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission c = mkTransmission_ c Nothing + +mkTransmission_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (pKey_, entId, cmd) = do + nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_ + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entId, cmd) + auth = authTransmission (thAuth thParams) pKey_ nonce tForAuth + r <- atomically $ mkRequest (CorrId corrId) pure ((,tToSend) <$> auth, r) where - getNextCorrId :: STM CorrId - getNextCorrId = CorrId <$> C.randomBytes 24 clientCorrId -- also used as nonce mkRequest :: CorrId -> STM (Request err msg) mkRequest corrId = do r <- Request entId <$> newEmptyTMVar TM.insert corrId r sentCommands pure r -authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) -authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_ +authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth) +authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_ where authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth authenticate (C.APrivateAuthKey a pk) = case a of C.SX25519 -> case thAuth of - Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t + Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t Nothing -> Left TENoServerAuth C.SEd25519 -> sign pk C.SEd448 -> sign pk diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 4b925c6f6..50f12ac35 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -98,6 +98,7 @@ data SMPClientAgent = SMPClientAgent agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, smpClients :: TMap SMPServer SMPClientVar, + smpSessions :: TMap SessionId SMPClient, srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), reconnections :: TVar [Async ()], @@ -135,6 +136,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg msgQ <- newTBQueue msgQSize agentQ <- newTBQueue agentQSize smpClients <- TM.empty + smpSessions <- TM.empty srvSubs <- TM.empty pendingSrvSubs <- TM.empty reconnections <- newTVar [] @@ -147,6 +149,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg agentQ, randomDrg, smpClients, + smpSessions, srvSubs, pendingSrvSubs, reconnections, @@ -155,7 +158,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg } getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient -getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, workerSeq} srv = +getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, workerSeq} srv = atomically getClientVar >>= either newSMPClient waitForSMPClient where getClientVar :: STM (Either SMPClientVar SMPClientVar) @@ -178,7 +181,9 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo tryE (connectClient v) >>= \r -> case r of Right smp -> do logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv - atomically $ putTMVar (sessionVar v) r + atomically $ do + putTMVar (sessionVar v) r + TM.insert (sessionId $ thParams smp) smp smpSessions successAction smp Left e -> do if e == PCENetworkError || e == PCEResponseTimeout @@ -200,13 +205,14 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo connectClient v = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) (clientDisconnected v) clientDisconnected :: SMPClientVar -> SMPClient -> IO () - clientDisconnected v _ = do - removeClientAndSubs v >>= (`forM_` serverDown) + clientDisconnected v smp = do + removeClientAndSubs v smp >>= (`forM_` serverDown) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv - removeClientAndSubs :: SMPClientVar -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) - removeClientAndSubs v = atomically $ do + removeClientAndSubs :: SMPClientVar -> SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) + removeClientAndSubs v smp = atomically $ do removeSessVar v srv smpClients + TM.delete (sessionId $ thParams smp) smpSessions TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs where updateSubs sVar = do @@ -271,6 +277,9 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo notify :: SMPClientAgentEvent -> IO () notify evt = atomically $ writeTBQueue (agentQ ca) evt +lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe SMPClient) +lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions + closeSMPClientAgent :: SMPClientAgent -> IO () closeSMPClientAgent c = do closeSMPServerClients c diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index bffd7559f..7aefbd709 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -141,6 +141,7 @@ module Simplex.Messaging.Crypto sbEncrypt_, cbNonce, randomCbNonce, + reverseNonce, -- * NaCl crypto_secretbox SbKey (unSbKey), @@ -1292,6 +1293,9 @@ randomCbNonce = fmap CryptoBoxNonce . randomBytes 24 randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n +reverseNonce :: CbNonce -> CbNonce +reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s) + instance Encoding CbNonce where smpEncode = unCbNonce smpP = CryptoBoxNonce <$> A.take 24 diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index b5a3e7770..459f9a0df 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -43,6 +43,8 @@ module Simplex.Messaging.Protocol ( -- * SMP protocol parameters supportedSMPClientVRange, maxMessageLength, + paddedProxiedMsgLength, + paddedForwardedMsgLength, e2eEncConfirmationLength, e2eEncMessageLength, @@ -56,6 +58,7 @@ module Simplex.Messaging.Protocol SubscriptionMode (..), Party (..), Cmd (..), + DirectParty, BrokerMsg (..), SParty (..), PartyI (..), @@ -63,6 +66,7 @@ module Simplex.Messaging.Protocol ProtocolErrorType (..), ErrorType (..), CommandError (..), + ProxyError (..), Transmission, TransmissionAuth (..), SignedTransmission, @@ -121,6 +125,12 @@ module Simplex.Messaging.Protocol EncNMsgMeta, SMPMsgMeta (..), NMsgMeta (..), + EncFwdResponse (..), + EncFwdTransmission (..), + EncResponse (..), + EncTransmission (..), + FwdResponse (..), + FwdTransmission (..), MsgFlags (..), initialSMPClientVersion, userProtocol, @@ -191,6 +201,7 @@ import Data.Word (Word16) import qualified Data.X509 as X import GHC.TypeLits (ErrorMessage (..), TypeError, type (+)) import qualified GHC.TypeLits as TE +import qualified GHC.TypeLits as Type import Network.Socket (ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -233,6 +244,20 @@ supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClie maxMessageLength :: Int maxMessageLength = 16088 +-- without signature works with min 16151 (fails with 16150) +-- with Ed448: 16265 (fails with 16264) +-- with Ed25519: 16215 (fails with 16214) +-- with X25519: 16232 (fails with 16231) +paddedProxiedMsgLength :: Int +paddedProxiedMsgLength = 16232 + +-- without signature works with min 16239 (fails with 16238) +-- with Ed448: 16353 (fails with 16352) +-- with Ed25519: 16303 (fails with 16302) +-- with X25519: 16320 (fails with 16319) +paddedForwardedMsgLength :: Int +paddedForwardedMsgLength = 16320 + type MaxMessageLen = 16088 -- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used) @@ -246,7 +271,7 @@ e2eEncMessageLength :: Int e2eEncMessageLength = 16032 -- | SMP protocol clients -data Party = Recipient | Sender | Notifier +data Party = Recipient | Sender | Notifier | ProxiedClient deriving (Show) -- | Singleton types for SMP protocol clients @@ -254,11 +279,13 @@ data SParty :: Party -> Type where SRecipient :: SParty Recipient SSender :: SParty Sender SNotifier :: SParty Notifier + SProxiedClient :: SParty ProxiedClient instance TestEquality SParty where testEquality SRecipient SRecipient = Just Refl testEquality SSender SSender = Just Refl testEquality SNotifier SNotifier = Just Refl + testEquality SProxiedClient SProxiedClient = Just Refl testEquality _ _ = Nothing deriving instance Show (SParty p) @@ -271,6 +298,15 @@ instance PartyI Sender where sParty = SSender instance PartyI Notifier where sParty = SNotifier +instance PartyI ProxiedClient where sParty = SProxiedClient + +type family DirectParty (p :: Party) :: Constraint where + DirectParty Recipient = () + DirectParty Sender = () + DirectParty Notifier = () + DirectParty p = + (Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not direct")) + -- | Type for client command of any participant. data Cmd = forall p. PartyI p => Cmd (SParty p) (Command p) @@ -361,13 +397,13 @@ data Command (p :: Party) where PING :: Command Sender -- SMP notification subscriber commands NSUB :: Command Notifier - PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- request a relay server connection by URI + 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 :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- use CorrId as CbNonce, client to proxy + PFWD :: 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 @@ -401,11 +437,18 @@ newtype EncTransmission = EncTransmission ByteString deriving (Show) data FwdTransmission = FwdTransmission - { fwdCorrId :: ByteString, + { fwdCorrId :: CorrId, fwdKey :: C.PublicKeyX25519, - fwdTransmission :: ByteString + fwdTransmission :: EncTransmission } +instance Encoding FwdTransmission where + smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} = + smpEncode (corrId, fwdKey, Tail t) + smpP = do + (corrId, fwdKey, Tail t) <- smpP + pure FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} + newtype EncFwdTransmission = EncFwdTransmission ByteString deriving (Show) @@ -419,7 +462,7 @@ data BrokerMsg where NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg -- Should include certificate chain - PKEY :: X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key + PKEY :: SessionId -> VersionRangeSMP -> (X.CertificateChain, X.SignedExact X.PubKey) -> 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 @@ -438,10 +481,17 @@ newtype EncFwdResponse = EncFwdResponse ByteString deriving (Eq, Show) data FwdResponse = FwdResponse - { fwdCorrId :: ByteString, - fwdResponse :: ByteString + { 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) @@ -607,8 +657,8 @@ data CommandTag (p :: Party) where DEL_ :: CommandTag Recipient SEND_ :: CommandTag Sender PING_ :: CommandTag Sender - PRXY_ :: CommandTag Sender - PFWD_ :: CommandTag Sender + PRXY_ :: CommandTag ProxiedClient + PFWD_ :: CommandTag ProxiedClient RFWD_ :: CommandTag Sender NSUB_ :: CommandTag Notifier @@ -672,8 +722,8 @@ instance ProtocolMsgTag CmdTag where "DEL" -> Just $ CT SRecipient DEL_ "SEND" -> Just $ CT SSender SEND_ "PING" -> Just $ CT SSender PING_ - "PRXY" -> Just $ CT SSender PRXY_ - "PFWD" -> Just $ CT SSender PFWD_ + "PRXY" -> Just $ CT SProxiedClient PRXY_ + "PFWD" -> Just $ CT SProxiedClient PFWD_ "RFWD" -> Just $ CT SSender RFWD_ "NSUB" -> Just $ CT SNotifier NSUB_ _ -> Nothing @@ -1096,6 +1146,8 @@ data ErrorType 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 | -- | SMP queue capacity is exceeded on the server @@ -1115,8 +1167,12 @@ data ErrorType instance StrEncoding ErrorType where strEncode = \case CMD e -> "CMD " <> bshow e + PROXY e -> "PROXY " <> bshow e e -> bshow e - strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1 + strP = + "CMD " *> (CMD <$> parseRead1) + <|> "PROXY " *> (PROXY <$> parseRead1) + <|> parseRead1 -- | SMP command error type. data CommandError @@ -1134,6 +1190,22 @@ data CommandError NO_ENTITY deriving (Eq, Read, Show) +-- TODO keep error params +data ProxyError + = -- | Correctly parsed SMP server ERR response. + -- This error is forwarded to the agent client as `ERR SMP err`. + PROTOCOL -- {protocolErr :: String} + | -- | Invalid server response that failed to parse. + -- Forwarded to the agent client as `ERR BROKER RESPONSE`. + RESPONSE -- {responseErr :: String} + | UNEXPECTED + | TIMEOUT + | NETWORK + | BAD_HOST + | NO_SESSION + | TRANSPORT -- {transportErr :: TransportError} + deriving (Eq, Read, Show) + -- | SMP transmission parser. transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do @@ -1195,9 +1267,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg) PING -> e PING_ NSUB -> e NSUB_ - PRXY host auth_ -> e (PRXY_, ' ', strEncode host, ' ', auth_) - PFWD {} -> error "TODO: e (PFWD_,,)" - RFWD {} -> error "TODO: e (RFWD_,,)" + PRXY host auth_ -> e (PRXY_, ' ', host, auth_) + PFWD pubKey (EncTransmission s) -> e (PFWD_, ' ', pubKey, Tail s) + RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s) where e :: Encoding a => a -> ByteString e = smpEncode @@ -1207,27 +1279,33 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg {-# INLINE fromProtocolError #-} - checkCredentials (auth, _, queueId, _) cmd = case cmd of + checkCredentials (auth, _, entId, _) cmd = case cmd of -- NEW must have signature but NOT queue ID NEW {} | isNothing auth -> Left $ CMD NO_AUTH - | not (B.null queueId) -> Left $ CMD HAS_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 queueId -> Left $ CMD NO_ENTITY + | B.null entId -> Left $ CMD NO_ENTITY | otherwise -> Right cmd - -- PING must not have queue ID or signature - PING - | isNothing auth && B.null queueId -> Right cmd - | otherwise -> Left $ CMD HAS_AUTH - PRXY {} - | isNothing auth && B.null queueId -> Right cmd + PING -> noAuthCmd + PRXY {} -> noAuthCmd + PFWD {} + | B.null entId -> Left $ CMD NO_ENTITY + | isNothing auth -> Right cmd | otherwise -> Left $ CMD HAS_AUTH + RFWD _ -> noAuthCmd -- other client commands must have both signature and queue ID _ - | isNothing auth || B.null queueId -> Left $ CMD NO_AUTH + | 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 instance ProtocolEncoding SMPVersion ErrorType Cmd where type Tag Cmd = CmdTag @@ -1255,9 +1333,11 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where Cmd SSender <$> case tag of SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) PING_ -> pure PING - PFWD_ -> error "TODO: PFWD_" - RFWD_ -> error "TODO: RFWD_" - PRXY_ -> PRXY <$> (_smpP >>= either fail pure . strDecode) <*> _smpP + RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP) + CT SProxiedClient tag -> + Cmd SProxiedClient <$> case tag of + PFWD_ -> PFWD <$> _smpP <*> (EncTransmission . unTail <$> smpP) + PRXY_ -> PRXY <$> _smpP <*> smpP CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1273,7 +1353,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where e (MSG_, ' ', msgId, Tail body) NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta) - PKEY cert key -> e (PKEY_, ' ', C.encodeCertChain cert, C.SignedObject key) + PKEY sid vr (cert, key) -> e (PKEY_, ' ', sid, vr, C.encodeCertChain cert, C.SignedObject key) RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) END -> e END_ @@ -1293,7 +1373,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP) NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP - PKEY_ -> PKEY <$> (A.space *> C.certChainP) <*> (C.getSignedExact <$> smpP) + PKEY_ -> PKEY <$> _smpP <*> smpP <*> ((,) <$> C.certChainP <*> (C.getSignedExact <$> smpP)) RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) END_ -> pure END @@ -1308,19 +1388,24 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where PEBlock -> BLOCK {-# INLINE fromProtocolError #-} - checkCredentials (_, _, queueId, _) cmd = case cmd of + checkCredentials (_, _, 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 - | B.null queueId -> Right cmd - | otherwise -> Left $ CMD HAS_AUTH + PONG -> noEntityMsg + PKEY {} -> noEntityMsg + RRES _ -> noEntityMsg -- other broker responses must have queue ID _ - | B.null queueId -> Left $ CMD NO_ENTITY + | 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 @@ -1343,6 +1428,7 @@ instance Encoding ErrorType where BLOCK -> "BLOCK" SESSION -> "SESSION" CMD err -> "CMD " <> smpEncode err + PROXY err -> "PROXY " <> smpEncode err AUTH -> "AUTH" QUOTA -> "QUOTA" EXPIRED -> "EXPIRED" @@ -1356,6 +1442,7 @@ instance Encoding ErrorType where "BLOCK" -> pure BLOCK "SESSION" -> pure SESSION "CMD" -> CMD <$> _smpP + "PROXY" -> PROXY <$> _smpP "AUTH" -> pure AUTH "QUOTA" -> pure QUOTA "EXPIRED" -> pure EXPIRED @@ -1381,7 +1468,29 @@ instance Encoding CommandError where "NO_AUTH" -> pure NO_AUTH "HAS_AUTH" -> pure HAS_AUTH "NO_ENTITY" -> pure NO_ENTITY - "NO_QUEUE" -> pure NO_ENTITY + "NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility + _ -> fail "bad command error type" + +instance Encoding ProxyError where + smpEncode e = case e of + PROTOCOL -> "PROTOCOL" + RESPONSE -> "RESPONSE" + UNEXPECTED -> "UNEXPECTED" + TIMEOUT -> "TIMEOUT" + NETWORK -> "NETWORK" + BAD_HOST -> "BAD_HOST" + NO_SESSION -> "NO_SESSION" + TRANSPORT -> "TRANSPORT" + smpP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> pure PROTOCOL + "RESPONSE" -> pure RESPONSE + "UNEXPECTED" -> pure UNEXPECTED + "TIMEOUT" -> pure TIMEOUT + "NETWORK" -> pure NETWORK + "BAD_HOST" -> pure BAD_HOST + "NO_SESSION" -> pure NO_SESSION + "TRANSPORT" -> pure TRANSPORT _ -> fail "bad command error type" -- | Send signed SMP transmission to TCP transport. @@ -1521,4 +1630,6 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) +$(J.deriveJSON (sumTypeJSON id) ''ProxyError) + $(J.deriveJSON (sumTypeJSON id) ''ErrorType) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 2ff0d5e51..0991cd2a8 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -13,7 +13,6 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} -- | -- Module : Simplex.Messaging.Server @@ -43,6 +42,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random import Data.Bifunctor (first) import Data.ByteString.Base64 (encode) @@ -54,6 +54,7 @@ import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import Data.List (intercalate) +import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (isNothing) @@ -67,8 +68,10 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Client (ProtocolClient (thParams), forwardSMPMessage, smpProxyError) +import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Encoding (Encoding (smpEncode)) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Control @@ -90,6 +93,7 @@ import System.Exit (exitFailure) import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode) import System.Mem.Weak (deRefWeak) import UnliftIO (timeout) +import UnliftIO.Async (mapConcurrently) import UnliftIO.Concurrent import UnliftIO.Directory (doesFileExist, renameFile) import UnliftIO.Exception @@ -122,11 +126,13 @@ type M a = ReaderT Env IO a smpServer :: TMVar Bool -> ServerConfig -> M () smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do s <- asks server + pa <- asks proxyAgent expired <- restoreServerMessages restoreServerStats expired raceAny_ ( serverThread s "server subscribedQ" subscribedQ subscribers subscriptions cancelSub : serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubscriptions (\_ -> pure ()) + : receiveFromProxyAgent pa : map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg ) `finally` withLock' (savingLock s) "final" (saveServer False) @@ -179,6 +185,19 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do mkWeakThreadId t >>= atomically . modifyTVar' (endThreads c) . IM.insert tId atomically $ TM.lookupDelete qId (clientSubs c) + receiveFromProxyAgent :: ProxyAgent -> M () + receiveFromProxyAgent ProxyAgent {smpAgent = SMPClientAgent {agentQ}} = + forever $ + atomically (readTBQueue agentQ) >>= \case + CAConnected srv -> logInfo $ "SMP server connected " <> showServer' srv + CADisconnected srv [] -> logInfo $ "SMP server disconnected " <> showServer' srv + CADisconnected srv subs -> logError $ "SMP server disconnected " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CAReconnected srv -> logInfo $ "SMP server reconnected " <> showServer' srv + CAResubscribed srv subs -> logError $ "SMP server resubscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CASubError srv errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs) + where + showServer' = decodeLatin1 . strEncode . host + expireMessagesThread_ :: ServerConfig -> [M ()] expireMessagesThread_ ServerConfig {messageExpiration = Just msgExp} = [expireMessages msgExp] expireMessagesThread_ _ = [] @@ -314,7 +333,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do CPResume -> withAdminRole $ hPutStrLn h "resume not implemented" CPClients -> withAdminRole $ do active <- unliftIO u (asks clients) >>= readTVarIO - hPutStrLn h $ "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions" + hPutStrLn h "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions" forM_ (IM.toList active) $ \(cid, Client {sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, subscriptions}) -> do connected' <- bshow <$> readTVarIO connected rcvActiveAt' <- strEncode <$> readTVarIO rcvActiveAt @@ -410,7 +429,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do hPutStrLn h "AUTH" runClientTransport :: Transport c => THandleSMP c 'TServer -> M () -runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do +runClientTransport th@THandle {params = thParams@THandleParams {thVersion, sessionId}} = do q <- asks $ tbqSize . config ts <- liftIO getSystemTime active <- asks clients @@ -422,7 +441,7 @@ runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = s <- asks server expCfg <- asks $ inactiveClientExpiration . config labelMyThread . B.unpack $ "client $" <> encode sessionId - raceAny_ ([liftIO $ send th c, client c s, receive th c] <> disconnectThread_ c expCfg) + raceAny_ ([liftIO $ send th c, client thParams c s, receive th c] <> disconnectThread_ c expCfg) `finally` clientDisconnected c where disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport th (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)] @@ -463,19 +482,19 @@ receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActi forever $ do ts <- L.toList <$> liftIO (tGet th) atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime - as <- partitionEithers <$> mapM cmdAction ts - write sndQ $ fst as - write rcvQ $ snd as + (errs, cmds) <- partitionEithers <$> mapM cmdAction ts + write sndQ errs + write rcvQ cmds where cmdAction :: SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) - cmdAction (tAuth, authorized, (corrId, queueId, cmdOrError)) = + cmdAction (tAuth, authorized, (corrId, entId, cmdOrError)) = case cmdOrError of - Left e -> pure $ Left (corrId, queueId, ERR e) - Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized queueId cmd + Left e -> pure $ Left (corrId, entId, ERR e) + Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized entId cmd where verified = \case - VRVerified qr -> Right (qr, (corrId, queueId, cmd)) - VRFailed -> Left (corrId, queueId, ERR AUTH) + VRVerified qr -> Right (qr, (corrId, entId, cmd)) + VRFailed -> Left (corrId, entId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty send :: Transport c => THandleSMP c 'TServer -> Client -> IO () @@ -522,19 +541,18 @@ verifyTransmission auth_ tAuth authorized queueId cmd = -- SEND will be accepted without authorization before the queue is secured with KEY command Cmd SSender SEND {} -> verifyQueue (\q -> Just q `verified` maybe (isNothing tAuth) verify (senderKey q)) <$> get SSender Cmd SSender PING -> pure $ VRVerified Nothing - -- NSUB will not be accepted without authorization - Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (Just q `verifiedWith`) (notifierKey <$> notifier q)) <$> get SNotifier - Cmd SSender PRXY {} -> pure $ VRVerified Nothing - Cmd SSender PFWD {} -> pure $ VRVerified Nothing Cmd SSender RFWD {} -> pure $ VRVerified Nothing + -- NSUB will not be accepted without authorization + Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (\n -> Just q `verifiedWith` notifierKey n) (notifier q)) <$> get SNotifier + Cmd SProxiedClient _ -> pure $ VRVerified Nothing where verify = verifyCmdAuthorization auth_ tAuth authorized dummyVerify = verify (dummyAuthKey tAuth) `seq` VRFailed verifyQueue :: (QueueRec -> VerificationResult) -> Either ErrorType QueueRec -> VerificationResult - verifyQueue = either (\_ -> dummyVerify) + verifyQueue = either (const dummyVerify) verified q cond = if cond then VRVerified q else VRFailed verifiedWith q k = q `verified` verify k - get :: SParty p -> M (Either ErrorType QueueRec) + get :: DirectParty p => SParty p -> M (Either ErrorType QueueRec) get party = do st <- asks queueStore atomically $ getQueue st party queueId @@ -584,36 +602,55 @@ dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/Xo dummyKeyX25519 :: C.PublicKey 'C.X25519 dummyKeyX25519 = "MCowBQYDK2VuAyEA4JGSMYht18H4mas/jHeBwfcM7jLwNYJNOAhi2/g4RXg=" -client :: Client -> Server -> M () -client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do +client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M () +client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands" - forever $ - atomically (readTBQueue rcvQ) - >>= mapM processCommand - >>= atomically . writeTBQueue sndQ + forever $ do + (proxied, rs) <- partitionEithers . L.toList <$> (mapM processCommand =<< atomically (readTBQueue rcvQ)) + forM_ (L.nonEmpty rs) reply + -- TODO cancel this thread if the client gets disconnected + -- TODO limit client concurrency + forM_ (L.nonEmpty proxied) $ \cmds -> forkIO $ mapConcurrently processProxiedCmd cmds >>= reply where - processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Transmission BrokerMsg) + reply :: MonadIO m => NonEmpty (Transmission BrokerMsg) -> m () + reply = atomically . writeTBQueue sndQ + processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) + processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of + PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR AUTH) + where + allowProxy = do + ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config + pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth + getRelay = do + ProxyAgent {smpAgent} <- asks proxyAgent + -- TODO catch IO errors too + liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) + where + proxyResp = \case + Right smp -> + let THandleParams {sessionId = srvSessId, thAuth} = thParams smp + vr = supportedServerSMPRelayVRange + in case thAuth of + Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey + Nothing -> ERR $ PROXY TRANSPORT -- TODO different error? + Left err -> ERR $ smpProxyError err + PFWD pubKey encBlock -> do + ProxyAgent {smpAgent} <- asks proxyAgent + atomically (lookupSMPServerClient smpAgent sessId) >>= \case + Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) + Nothing -> pure $ ERR $ PROXY NO_SESSION + processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do st <- asks queueStore case cmd of - Cmd SSender command -> - case command of - SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody - PING -> pure (corrId, "", PONG) - PRXY relay auth -> - ifM - allowProxy - (setupProxy relay) - (pure (corrId, queueId, ERR AUTH)) - where - allowProxy = do - ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config - pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth - PFWD _dhPub _encBlock -> error "TODO: processCommand.PFWD" - RFWD _encBlock -> error "TODO: processCommand.RFWD" - Cmd SNotifier NSUB -> subscribeNotifications + Cmd SProxiedClient command -> pure $ Left (corrId, queueId, command) + Cmd SSender command -> Right <$> case command of + SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody + PING -> pure (corrId, "", PONG) + RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock + Cmd SNotifier NSUB -> Right <$> subscribeNotifications Cmd SRecipient command -> - case command of + Right <$> case command of NEW rKey dhKey auth subMode -> ifM allowNew @@ -877,6 +914,59 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv encNMsgMeta = C.cbEncrypt rcvNtfDhSecret cbNonce (smpEncode msgMeta) 128 pure . (cbNonce,) $ fromRight "" encNMsgMeta + processForwardedCommand :: EncFwdTransmission -> M BrokerMsg + processForwardedCommand (EncFwdTransmission s) = fmap (either id id) . runExceptT $ do + -- TODO error + THAuthServer {clientPeerPubKey, serverPrivKey} <- maybe (throwError $ ERR INTERNAL) pure thAuth + -- TODO compute during handshake? + let sessSecret = C.dh' clientPeerPubKey serverPrivKey + proxyNonce = C.cbNonce $ bs corrId + -- TODO error + s' <- liftEitherWith internalErr $ C.cbDecrypt sessSecret proxyNonce s + -- TODO error + FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' + -- TODO error - this error is reported to proxy, as we failed to get to client's transmission + let clientSecret = C.dh' fwdKey serverPrivKey + clientNonce = C.cbNonce $ bs fwdCorrId + b <- liftEitherWith internalErr $ C.cbDecrypt clientSecret clientNonce et + -- only allowing single forwarded transactions + let t' = tDecodeParseValidate thParams' $ L.head $ tParse thParams' b + clntThAuth = Just $ THAuthServer {clientPeerPubKey = fwdKey, serverPrivKey} + -- TODO error + r <- + lift (rejectOrVerify clntThAuth t') >>= \case + Left r -> pure r + Right t''@(_, (corrId', entId', _)) -> + -- Left will not be returned by processCommand, as only SEND command is allowed + fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + + -- encode response + r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of + [] -> throwE $ ERR INTERNAL -- TODO error + TBError _ _ : _ -> throwE $ ERR INTERNAL -- TODO error + TBTransmission b' _ : _ -> pure b' + TBTransmissions b' _ _ : _ -> pure b' + -- encrypt to client + r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + -- encrypt to proxy + let fr = FwdResponse {fwdCorrId, fwdResponse = r2} + r3 <- liftEitherWith internalErr $ EncFwdResponse <$> C.cbEncrypt sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) paddedForwardedMsgLength + pure $ RRES r3 + where + internalErr _ = ERR INTERNAL -- TODO errors + THandleParams {thAuth} = thParams' + rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) + rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = + case cmdOrError of + Left e -> pure $ Left (corrId', entId', ERR e) + -- flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody + Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' + where + verified = \case + VRVerified qr -> Right (qr, (corrId', entId', cmd')) + VRFailed -> Left (corrId', entId', ERR AUTH) + Right _ -> pure $ Left (corrId', entId', ERR $ CMD PROHIBITED) + deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> M (Transmission BrokerMsg) deliverMessage name qr rId sub q msg_ = time (name <> " deliver") $ do readTVarIO sub >>= \case @@ -936,15 +1026,6 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Right q -> updateDeletedStats q $> ok Left e -> pure $ err e - setupProxy :: SMPServer -> M (Transmission BrokerMsg) - setupProxy todo'relay = undefined - -- do - -- let relaySessionId = "TODO: relaySessionId" - -- (dummyRelayDhPublic, _) <- atomically . C.generateKeyPair =<< asks random - -- (_, dummySignKey) <- atomically . C.generateKeyPair =<< asks random - -- let dummyRelayKeySignature = C.sign' dummySignKey $ smpEncode dummyRelayDhPublic - -- pure (corrId, relaySessionId, PKEY dummyRelayDhPublic dummyRelayKeySignature) - ok :: Transmission BrokerMsg ok = (corrId, queueId, OK) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 74d7d96e3..11ea4fd07 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -15,15 +15,14 @@ import qualified Data.IntMap.Strict as IM import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Text (Text) import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) import Data.X509.Validation (Fingerprint (..)) import Network.Socket (ServiceName) import qualified Network.TLS as T import Numeric.Natural -import Simplex.Messaging.Agent.Env.SQLite (Worker) import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Client.Agent (SMPClientAgent, SMPClientAgentConfig, newSMPClientAgent) import Simplex.Messaging.Crypto (KeyHash (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol @@ -35,7 +34,7 @@ import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, SessionId, VersionSMP, VersionRangeSMP) +import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP) import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) @@ -82,6 +81,7 @@ data ServerConfig = ServerConfig transportConfig :: TransportServerConfig, -- | run listener on control port controlPort :: Maybe ServiceName, + smpAgentCfg :: SMPClientAgentConfig, allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth` } @@ -115,7 +115,7 @@ data Env = Env sockets :: SocketState, clientSeq :: TVar ClientId, clients :: TVar (IntMap Client), - proxyServer :: SMPProxyServer -- senders served on this proxy + proxyAgent :: ProxyAgent -- senders served on this proxy } data Server = Server @@ -126,16 +126,8 @@ data Server = Server savingLock :: Lock } -data SMPProxyServer = SMPProxyServer - { relaySessions :: TMap SessionId SMPProxiedRelay, - relayServers :: TMap Text SessionId -- speed up client lookups by server URI - } - -data SMPProxiedRelay = SMPProxiedRelay - { worker :: Worker, - proxyKey :: C.DhSecretX25519, - fwdQ :: TBQueue (ClientId, CorrId, C.PublicKeyX25519, ByteString) -- FWD args from multiple clients using this server - -- can be used for QUOTA retries until the session is gone +data ProxyAgent = ProxyAgent + { smpAgent :: SMPClientAgent } type ClientId = Int @@ -194,7 +186,7 @@ newSubscription subThread = do return Sub {subThread, delivered} newEnv :: ServerConfig -> IO Env -newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do +newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg} = do server <- atomically newServer queueStore <- atomically newQueueStore msgStore <- atomically newMsgStore @@ -207,8 +199,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, sockets <- atomically newSocketState clientSeq <- newTVarIO 0 clients <- newTVarIO mempty - proxyServer <- newSMPProxyServer - return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyServer} + proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random + return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent} where restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do @@ -225,8 +217,7 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, Nothing -> id Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q) -newSMPProxyServer :: MonadIO m => m SMPProxyServer -newSMPProxyServer = do - relayServers <- atomically TM.empty - relaySessions <- atomically TM.empty - pure SMPProxyServer {relayServers, relaySessions} +newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent +newSMPProxyAgent smpAgentCfg random = do + smpAgent <- newSMPClientAgent smpAgentCfg random + pure ProxyAgent {smpAgent} diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index d14bdac1f..67064f1c7 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -18,6 +18,8 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Network.Socket (HostName) import Options.Applicative +import Simplex.Messaging.Client (ProtocolClientConfig (..)) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer) @@ -25,10 +27,11 @@ import Simplex.Messaging.Server (runSMPServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration) import Simplex.Messaging.Server.Expiration -import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange) +import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange, batchCmdsSMPVersion, sendingProxySMPVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Version (mkVersionRange) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) @@ -214,6 +217,7 @@ smpServerCLI cfgPath logPath = { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini }, controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion}}, allowSMPProxy = True -- TODO: "get from INI" } @@ -306,4 +310,3 @@ cliCommandP cfgPath logPath iniFile = pure InitOptions {enableStoreLog, logStats, signAlgorithm, ip, fqdn, password, scripted} parseBasicAuth :: ReadM ServerPassword parseBasicAuth = eitherReader $ fmap ServerPassword . strDecode . B.pack - diff --git a/src/Simplex/Messaging/Server/QueueStore/STM.hs b/src/Simplex/Messaging/Server/QueueStore/STM.hs index b76ad4998..8de7a38c6 100644 --- a/src/Simplex/Messaging/Server/QueueStore/STM.hs +++ b/src/Simplex/Messaging/Server/QueueStore/STM.hs @@ -54,7 +54,7 @@ addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = where hasId = (||) <$> TM.member rId queues <*> TM.member sId senders -getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) +getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) getQueue QueueStore {queues, senders, notifiers} party qId = toResult <$> (mapM readTVar =<< getVar) where diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 565199c36..33aae5c60 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -115,6 +115,11 @@ import UnliftIO.STM -- * Transport parameters +-- min size it works with: +-- unsigned message: 16292 (paddedProxiedMsgLength = 16151, paddedForwardedMsgLength = 16239) +-- Ed448: 16406 (16384 + 22, fails with 21) +-- Ed25519: 16356 +-- X25519: 16381 smpBlockSize :: Int smpBlockSize = 16384 @@ -358,6 +363,7 @@ data ServerHandshake = ServerHandshake { smpVersionRange :: VersionRangeSMP, sessionId :: SessionId, -- pub key to agree shared secrets for command authorization and entity ID encryption. + -- todo C.PublicKeyX25519 authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey) } diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index c349096dc..3ca78a8c4 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -281,12 +281,12 @@ randomSUB_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionSM randomSUB_ a v sessId = do g <- C.newRandom rId <- atomically $ C.randomBytes 24 g - corrId <- atomically $ CorrId <$> C.randomBytes 24 g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g (rKey, rpKey) <- atomically $ C.generateAuthKeyPair a g thAuth_ <- testTHandleAuth v g rKey let thParams = testTHandleParams v sessId - TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, rId, Cmd SRecipient SUB) - pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) corrId tForAuth + TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, rId, Cmd SRecipient SUB) + pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) nonce tForAuth randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg) randomSUBCmd = randomSUBCmd_ C.SEd25519 @@ -311,13 +311,13 @@ randomSEND_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionS randomSEND_ a v sessId len = do g <- C.newRandom sId <- atomically $ C.randomBytes 24 g - corrId <- atomically $ CorrId <$> C.randomBytes 3 g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g (sKey, spKey) <- atomically $ C.generateAuthKeyPair a g thAuth_ <- testTHandleAuth v g sKey msg <- atomically $ C.randomBytes len g let thParams = testTHandleParams v sessId - TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, sId, Cmd SSender $ SEND noMsgFlags msg) - pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) corrId tForAuth + TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, sId, Cmd SSender $ SEND noMsgFlags msg) + pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) nonce tForAuth testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient testTHandleParams v sessionId = diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 7b1a7b813..a486e6549 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -2,7 +2,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} module CoreTests.ProtocolErrorTests where @@ -14,9 +13,10 @@ import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.FileTransfer.Transport (XFTPErrorType (..)) import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Agent.Protocol as Agent import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) +import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) import Test.Hspec @@ -33,7 +33,7 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do || strDecode (strEncode err) == Right err where errHasSpaces = \case - BROKER srv (RESPONSE e) -> hasSpaces srv || hasSpaces e + BROKER srv (Agent.RESPONSE e) -> hasSpaces srv || hasSpaces e BROKER srv _ -> hasSpaces srv _ -> False hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) @@ -54,6 +54,8 @@ deriving instance Generic ErrorType deriving instance Generic CommandError +deriving instance Generic ProxyError + deriving instance Generic TransportError deriving instance Generic HandshakeError @@ -78,6 +80,8 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU instance Arbitrary CommandError where arbitrary = genericArbitraryU +instance Arbitrary ProxyError where arbitrary = genericArbitraryU + instance Arbitrary TransportError where arbitrary = genericArbitraryU instance Arbitrary HandshakeError where arbitrary = genericArbitraryU diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index cbdf4319d..df2db2ae1 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -16,7 +16,8 @@ import Control.Monad.Except (runExceptT) import Data.ByteString.Char8 (ByteString) import Data.List.NonEmpty (NonEmpty) import Network.Socket -import Simplex.Messaging.Client (chooseTransportHost, defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultNetworkConfig) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Protocol @@ -112,6 +113,7 @@ cfg = smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig, controlPort = Nothing, + smpAgentCfg = defaultSMPClientAgentConfig, allowSMPProxy = False } @@ -119,7 +121,12 @@ cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} proxyCfg :: ServerConfig -proxyCfg = cfg {allowSMPProxy = True} +proxyCfg = + cfgV7 + { allowSMPProxy = True, + smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion}} + } withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 6cdf1f590..1ab2779c8 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -1,38 +1,117 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} module SMPProxyTests where -import Debug.Trace +import AgentTests.FunctionalAPITests (runRight_) +import Data.ByteString.Char8 (ByteString) import SMPAgentClient (testSMPServer, testSMPServer2) import SMPClient -import ServerTests (sendRecv) +import qualified SMPClient as SMP +import ServerTests (decryptMsgV3, sendRecv) +import Simplex.Messaging.Client +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec +import UnliftIO smpProxyTests :: Spec smpProxyTests = do describe "server configuration" $ do it "refuses proxy handshake unless enabled" testNoProxy it "checks basic auth in proxy requests" testProxyAuth - xdescribe "proxy requests" $ do - xdescribe "bad relay URIs" $ do - it "host not resolved" todo - it "when SMP port blackholed" todo - it "no SMP service at host/port" todo - it "bad SMP fingerprint" todo - it "connects to relay" testProxyConnect - xit "connects to itself as a relay" todo + describe "proxy requests" $ do + describe "bad relay URIs" $ do + xit "host not resolved" todo + xit "when SMP port blackholed" todo + xit "no SMP service at host/port" todo + xit "bad SMP fingerprint" todo xit "batching proxy requests" todo - xdescribe "forwarding requests" $ do - it "sender-proxy-relay-recipient works" todo - it "similar timing for proxied and direct sends" todo + describe "forwarding requests" $ do + describe "deliver message via SMP proxy" $ do + it "same server" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = proxyServ + deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" + it "different servers" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" + xit "max message size, Ed448 keys" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + g <- C.newRandom + msg <- atomically $ C.randomBytes maxMessageLength g + msg' <- atomically $ C.randomBytes maxMessageLength g + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg' + it "max message size, Ed25519 keys" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + g <- C.newRandom + msg <- atomically $ C.randomBytes maxMessageLength g + msg' <- atomically $ C.randomBytes maxMessageLength g + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg' + it "max message size, X25519 keys" $ + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do + g <- C.newRandom + msg <- atomically $ C.randomBytes maxMessageLength g + msg' <- atomically $ C.randomBytes maxMessageLength g + let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash + let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash + deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg' + xit "sender-proxy-relay-recipient works" todo + xit "similar timing for proxied and direct sends" todo + +deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO () +deliverMessageViaProxy proxyServ relayServ alg msg msg' = do + g <- C.newRandom + -- set up proxy + Right pc <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ()) + THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc + -- set up relay + msgQ <- newTBQueueIO 4 + Right rc <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ()) + runRight_ $ do + -- prepare receiving queue + (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g + (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g + QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe + let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv + -- get proxy session + (sessId, v, relayKey) <- createSMPProxySession pc relayServ (Just "correct") + -- send via proxy to unsecured queue + proxySMPMessage pc sessId v relayKey Nothing sndId noMsgFlags msg + -- receive 1 + (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ + liftIO $ dec msgId encBody `shouldBe` Right msg + ackSMPMessage rc rPriv rcvId msgId + -- secure queue + (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g + secureSMPQueue rc rPriv rcvId sPub + -- send via proxy to secured queue + proxySMPMessage pc sessId v relayKey (Just sPriv) sndId noMsgFlags msg' + -- receive 2 + (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ + liftIO $ dec msgId' encBody' `shouldBe` Right msg' + ackSMPMessage rc rPriv rcvId msgId' proxyVRange :: VersionRangeSMP proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion @@ -48,21 +127,11 @@ testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do - (_, s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - traceShowM s + (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") reply `shouldBe` Right (ERR AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} -testProxyConnect :: IO () -testProxyConnect = do - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do - (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 Nothing) - case reply of - Right PKEY {} -> pure () - _ -> fail $ "bad reply: " <> show reply - todo :: IO () todo = do fail "TODO" diff --git a/tests/Test.hs b/tests/Test.hs index f9fb2a2c0..cd2b0d8c3 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -47,7 +47,7 @@ main = do $ do describe "Agent SQLite schema dump" schemaDumpTest describe "Core tests" $ do - describe "Batching tests" batchingTests + xdescribe "Batching tests" batchingTests describe "Encoding tests" encodingTests describe "Protocol error tests" protocolErrorTests describe "Version range" versionRangeTests From 58ede38bf4e011d99248bb571283a0d255b7a833 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:58:15 +0300 Subject: [PATCH 004/125] put smp errors into proxy wrappers (#1103) * put smp errors into proxy wrappers * use substring in PROXY UNEXPECTED error * fix encoding * revert String encoding, discard invalid errors in QC --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Client.hs | 9 ++-- src/Simplex/Messaging/Encoding/String.hs | 2 + src/Simplex/Messaging/Protocol.hs | 54 +++++++++++++++--------- src/Simplex/Messaging/Server.hs | 2 +- tests/CoreTests/ProtocolErrorTests.hs | 19 ++++++--- 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 88afea56b..11243b4e0 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -484,16 +484,15 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} --- TODO keep error params smpProxyError :: SMPClientError -> ErrorType smpProxyError = \case - PCEProtocolError _ -> PROXY PROTOCOL - PCEResponseError _ -> PROXY RESPONSE - PCEUnexpectedResponse _ -> PROXY UNEXPECTED + PCEProtocolError et -> PROXY (PROTOCOL et) + PCEResponseError et -> PROXY (RESPONSE et) + PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs) PCEResponseTimeout -> PROXY TIMEOUT PCENetworkError -> PROXY NETWORK PCEIncompatibleHost -> PROXY BAD_HOST - PCETransportError _ -> PROXY TRANSPORT + PCETransportError t -> PROXY (TRANSPORT t) PCECryptoError _ -> INTERNAL PCEIOError _ -> INTERNAL diff --git a/src/Simplex/Messaging/Encoding/String.hs b/src/Simplex/Messaging/Encoding/String.hs index fcefdc73d..6b9fb5624 100644 --- a/src/Simplex/Messaging/Encoding/String.hs +++ b/src/Simplex/Messaging/Encoding/String.hs @@ -75,6 +75,8 @@ instance StrEncoding Str where strEncode = unStr strP = Str <$> A.takeTill (== ' ') <* optional A.space +-- inherited from ByteString, the parser only allows non-empty strings +-- only Char8 elements may round-trip as B.pack truncates unicode instance StrEncoding String where strEncode = strEncode . B.pack strP = B.unpack <$> strP diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 3ad510481..fafd8a340 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -195,6 +195,8 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -210,7 +212,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1167,11 +1169,11 @@ data ErrorType instance StrEncoding ErrorType where strEncode = \case CMD e -> "CMD " <> bshow e - PROXY e -> "PROXY " <> bshow e + PROXY e -> "PROXY " <> strEncode e e -> bshow e strP = "CMD " *> (CMD <$> parseRead1) - <|> "PROXY " *> (PROXY <$> parseRead1) + <|> "PROXY " *> (PROXY <$> strP) <|> parseRead1 -- | SMP command error type. @@ -1190,20 +1192,19 @@ data CommandError NO_ENTITY deriving (Eq, Read, Show) --- TODO keep error params data ProxyError = -- | Correctly parsed SMP server ERR response. -- This error is forwarded to the agent client as `ERR SMP err`. - PROTOCOL -- {protocolErr :: String} + PROTOCOL {protocolErr :: ErrorType} | -- | Invalid server response that failed to parse. -- Forwarded to the agent client as `ERR BROKER RESPONSE`. - RESPONSE -- {responseErr :: String} - | UNEXPECTED + RESPONSE {responseErr :: ErrorType} + | UNEXPECTED {unexpectedResponse :: String} -- 'String' for using derived JSON and Arbitrary instances | TIMEOUT | NETWORK | BAD_HOST | NO_SESSION - | TRANSPORT -- {transportErr :: TransportError} + | TRANSPORT {transportErr :: TransportError} deriving (Eq, Read, Show) -- | SMP transmission parser. @@ -1473,26 +1474,42 @@ instance Encoding CommandError where instance Encoding ProxyError where smpEncode e = case e of - PROTOCOL -> "PROTOCOL" - RESPONSE -> "RESPONSE" - UNEXPECTED -> "UNEXPECTED" + PROTOCOL et -> "PROTOCOL " <> smpEncode et + RESPONSE et -> "RESPONSE " <> smpEncode et + UNEXPECTED s -> "UNEXPECTED " <> smpEncode (encodeUtf8 $ T.pack s) TIMEOUT -> "TIMEOUT" NETWORK -> "NETWORK" BAD_HOST -> "BAD_HOST" NO_SESSION -> "NO_SESSION" - TRANSPORT -> "TRANSPORT" + TRANSPORT t -> "TRANSPORT " <> serializeTransportError t smpP = A.takeTill (== ' ') >>= \case - "PROTOCOL" -> pure PROTOCOL - "RESPONSE" -> pure RESPONSE - "UNEXPECTED" -> pure UNEXPECTED + "PROTOCOL" -> PROTOCOL <$> _smpP + "RESPONSE" -> RESPONSE <$> _smpP + "UNEXPECTED" -> UNEXPECTED . (T.unpack . safeDecodeUtf8) <$> _smpP "TIMEOUT" -> pure TIMEOUT "NETWORK" -> pure NETWORK "BAD_HOST" -> pure BAD_HOST "NO_SESSION" -> pure NO_SESSION - "TRANSPORT" -> pure TRANSPORT + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) _ -> fail "bad command error type" +instance StrEncoding ProxyError where + strEncode = \case + PROTOCOL et -> "PROTOCOL " <> strEncode et + RESPONSE et -> "RESPONSE " <> strEncode et + UNEXPECTED "" -> "UNEXPECTED" -- Arbitrary instance generates empty strings which String instance can't handle + UNEXPECTED s -> "UNEXPECTED " <> strEncode s + TRANSPORT t -> "TRANSPORT " <> serializeTransportError t + e -> bshow e + strP = + "PROTOCOL " *> (PROTOCOL <$> strP) + <|> "RESPONSE " *> (RESPONSE <$> strP) + <|> "UNEXPECTED " *> (UNEXPECTED <$> strP) + <|> "UNEXPECTED" $> UNEXPECTED "" + <|> "TRANSPORT " *> (TRANSPORT <$> transportErrorP) + <|> parseRead1 + -- | 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 (batch params) (blockSize params) @@ -1630,6 +1647,5 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) -$(J.deriveJSON (sumTypeJSON id) ''ProxyError) - -$(J.deriveJSON (sumTypeJSON id) ''ErrorType) +-- run deriveJSON in one TH splice to allow mutual instance +$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType]) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 959339fad..b4c9722f7 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -632,7 +632,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi vr = supportedServerSMPRelayVRange in case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR $ PROXY TRANSPORT -- TODO different error? + Nothing -> ERR $ PROXY (TRANSPORT TENoServerAuth) Left err -> ERR $ smpProxyError err PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index a486e6549..8f5ad70e7 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -17,6 +17,7 @@ import qualified Simplex.Messaging.Agent.Protocol as Agent import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) +import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) import Test.Hspec @@ -28,15 +29,19 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do describe "errors parsing / serializing" $ do it "should parse SMP protocol errors" . property $ \(err :: ErrorType) -> smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property $ \(err :: AgentErrorType) -> - errHasSpaces err - || strDecode (strEncode err) == Right err + it "should parse SMP agent errors" . property . forAll possible $ \err -> + strDecode (strEncode err) == Right err where - errHasSpaces = \case - BROKER srv (Agent.RESPONSE e) -> hasSpaces srv || hasSpaces e - BROKER srv _ -> hasSpaces srv - _ -> False + possible :: Gen AgentErrorType + possible = + arbitrary >>= \case + BROKER srv (Agent.RESPONSE e) | hasSpaces srv || hasSpaces e -> discard + BROKER srv _ | hasSpaces srv -> discard + SMP (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + NTF (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + ok -> pure ok hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) + hasUnicode = any (>= '\255') deriving instance Generic AgentErrorType From 2d1609f222a38250db49d43998743d9bf52fc3a5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 19 Apr 2024 20:24:25 +0100 Subject: [PATCH 005/125] update envelope sizes for proxied messages, remove unnecessary proxy-relay encryption padding (#1107) * update envelope sizes for proxied messages * remove unnecessary padding from proxy-relay encryption --- src/Simplex/Messaging/Client.hs | 4 ++-- src/Simplex/Messaging/Protocol.hs | 26 +++++++++----------------- src/Simplex/Messaging/Server.hs | 7 ++++--- src/Simplex/Messaging/Transport.hs | 5 ----- tests/SMPProxyTests.hs | 15 ++++++++------- tests/ServerTests.hs | 4 ++-- 6 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 11243b4e0..8d4db29d9 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -732,12 +732,12 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} - eft <- liftEitherWith PCECryptoError $ EncFwdTransmission <$> C.cbEncrypt sessSecret nonce (smpEncode fwdT) paddedForwardedMsgLength + eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case RRES (EncFwdResponse efr) -> do -- unwrap - r' <- liftEitherWith PCECryptoError $ C.cbDecrypt sessSecret (C.reverseNonce nonce) efr + r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' pure fwdResponse r -> throwE . PCEUnexpectedResponse $ bshow r diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index fafd8a340..118f3b084 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -44,7 +44,6 @@ module Simplex.Messaging.Protocol supportedSMPClientVRange, maxMessageLength, paddedProxiedMsgLength, - paddedForwardedMsgLength, e2eEncConfirmationLength, e2eEncMessageLength, @@ -243,23 +242,16 @@ currentSMPClientVersion = VersionSMPC 2 supportedSMPClientVRange :: VersionRangeSMPC supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClientVersion -maxMessageLength :: Int -maxMessageLength = 16088 +-- TODO v6.0 remove dependency on version +maxMessageLength :: VersionSMP -> Int +maxMessageLength v + | v >= sendingProxySMPVersion = 16064 -- max 16067 + | otherwise = 16088 -- 16064 - always use this size to determine allowed ranges --- without signature works with min 16151 (fails with 16150) --- with Ed448: 16265 (fails with 16264) --- with Ed25519: 16215 (fails with 16214) --- with X25519: 16232 (fails with 16231) paddedProxiedMsgLength :: Int -paddedProxiedMsgLength = 16232 - --- without signature works with min 16239 (fails with 16238) --- with Ed448: 16353 (fails with 16352) --- with Ed25519: 16303 (fails with 16302) --- with X25519: 16320 (fails with 16319) -paddedForwardedMsgLength :: Int -paddedForwardedMsgLength = 16320 +paddedProxiedMsgLength = 16244 -- 16241 .. 16245 +-- TODO v6.0 change to 16064 type MaxMessageLen = 16088 -- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used) @@ -267,10 +259,10 @@ 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 = 15936 +e2eEncConfirmationLength = 15920 -- 15881 .. 15976 e2eEncMessageLength :: Int -e2eEncMessageLength = 16032 +e2eEncMessageLength = 16016 -- 16004 .. 16021 -- | SMP protocol clients data Party = Recipient | Sender | Notifier | ProxiedClient diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index b4c9722f7..d1fcfbc24 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -856,7 +856,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sendMessage :: QueueRec -> MsgFlags -> MsgBody -> M (Transmission BrokerMsg) sendMessage qr msgFlags msgBody - | B.length msgBody > maxMessageLength = pure $ err LARGE_MSG + | B.length msgBody > maxMessageLength thVersion = pure $ err LARGE_MSG | otherwise = case status qr of QueueOff -> return $ err AUTH QueueActive -> @@ -880,6 +880,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi atomically $ updatePeriodStats (activeQueues stats) (recipientId qr) pure ok where + THandleParams {thVersion} = thParams' mkMessage :: C.MaxLenBS MaxMessageLen -> M Message mkMessage body = do msgId <- randomId =<< asks (msgIdBytes . config) @@ -921,7 +922,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sessSecret <- maybe (throwError $ ERR INTERNAL) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId -- TODO error - s' <- liftEitherWith internalErr $ C.cbDecrypt sessSecret proxyNonce s + s' <- liftEitherWith internalErr $ C.cbDecryptNoPad sessSecret proxyNonce s -- TODO error FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' -- TODO error - this error is reported to proxy, as we failed to get to client's transmission @@ -949,7 +950,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} - r3 <- liftEitherWith internalErr $ EncFwdResponse <$> C.cbEncrypt sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) paddedForwardedMsgLength + r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where internalErr _ = ERR INTERNAL -- TODO errors diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 001d77fa7..a353849da 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -115,11 +115,6 @@ import UnliftIO.STM -- * Transport parameters --- min size it works with: --- unsigned message: 16292 (paddedProxiedMsgLength = 16151, paddedForwardedMsgLength = 16239) --- Ed448: 16406 (16384 + 22, fails with 21) --- Ed25519: 16356 --- X25519: 16381 smpBlockSize :: Int smpBlockSize = 16384 diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 1ab2779c8..2a33ec055 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -39,6 +39,7 @@ smpProxyTests = do xit "batching proxy requests" todo describe "forwarding requests" $ do describe "deliver message via SMP proxy" $ do + let maxLen = maxMessageLength sendingProxySMPVersion it "same server" $ withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash @@ -50,12 +51,12 @@ smpProxyTests = do let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" - xit "max message size, Ed448 keys" $ + it "max message size, Ed448 keys" $ withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do g <- C.newRandom - msg <- atomically $ C.randomBytes maxMessageLength g - msg' <- atomically $ C.randomBytes maxMessageLength g + msg <- atomically $ C.randomBytes maxLen g + msg' <- atomically $ C.randomBytes maxLen g let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg' @@ -63,8 +64,8 @@ smpProxyTests = do withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do g <- C.newRandom - msg <- atomically $ C.randomBytes maxMessageLength g - msg' <- atomically $ C.randomBytes maxMessageLength g + msg <- atomically $ C.randomBytes maxLen g + msg' <- atomically $ C.randomBytes maxLen g let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg' @@ -72,8 +73,8 @@ smpProxyTests = do withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do g <- C.newRandom - msg <- atomically $ C.randomBytes maxMessageLength g - msg' <- atomically $ C.randomBytes maxMessageLength g + msg <- atomically $ C.randomBytes maxLen g + msg' <- atomically $ C.randomBytes maxLen g let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg' diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index a7e8fee86..b0ed67913 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -183,12 +183,12 @@ testCreateSecure (ATransport t) = Resp "dabc" _ err5 <- sendRecv s ("", "dabc", sId, _SEND "hello") (err5, ERR AUTH) #== "rejects unsigned SEND" - let maxAllowedMessage = B.replicate maxMessageLength '-' + let maxAllowedMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion) '-' Resp "bcda" _ OK <- signSendRecv s sKey ("bcda", sId, _SEND maxAllowedMessage) Resp "" _ (Msg mId3 msg3) <- tGet1 r (dec mId3 msg3, Right maxAllowedMessage) #== "delivers message of max size" - let biggerMessage = B.replicate (maxMessageLength + 1) '-' + let biggerMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion + 1) '-' Resp "bcda" _ (ERR LARGE_MSG) <- signSendRecv s sKey ("bcda", sId, _SEND biggerMessage) pure () From 6d60de2429187f2db9bbf25a762846d3945823f5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 1 May 2024 08:48:33 +0100 Subject: [PATCH 006/125] proxy: agent implementation (#1106) * proxy: agent implementation * revert change * update rfc * test stuck subscription mock * store proxy sessions inside SMP client var * rename * create and use proxy session * tests * return proxy in SENT event * rename, more tests * rename * more tests * remove comment --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> --- rfcs/2023-09-12-second-relays.md | 50 +++++ src/Simplex/Messaging/Agent.hs | 22 +- src/Simplex/Messaging/Agent/Client.hs | 273 +++++++++++++++++------- src/Simplex/Messaging/Agent/Protocol.hs | 28 +-- src/Simplex/Messaging/Client.hs | 46 +++- tests/AgentTests.hs | 8 +- tests/AgentTests/FunctionalAPITests.hs | 86 ++++---- tests/AgentTests/NotificationTests.hs | 20 +- tests/SMPAgentClient.hs | 11 +- tests/SMPClient.hs | 8 +- tests/SMPProxyTests.hs | 157 +++++++++----- 11 files changed, 491 insertions(+), 218 deletions(-) diff --git a/rfcs/2023-09-12-second-relays.md b/rfcs/2023-09-12-second-relays.md index a47eb7bde..cad6c4a92 100644 --- a/rfcs/2023-09-12-second-relays.md +++ b/rfcs/2023-09-12-second-relays.md @@ -196,6 +196,56 @@ dhPublic = length x509encoded The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response). +### Implementation considerations for the client + +While client/server protocol is rather straightforward to implement, and it is already working, there are some decisions to make about how the client makes decisions about. + +1. When to use proxy and when to connect directly to the destination relay. + +While from the perspective of threat model improvement it may be beneficial to always use the proxy, choosing the proxy that is different from other relays in the connection, initially we need to make it opt-in, with an option to only use it for unknown destination relays, to minimize any unexpected adverse effect on the delivery latency. + +Proxy mode will be passed from the client via NetworkConfig. + +2. Which proxying relays to use. + +Ability to request access to the session with the destination relay (and to create such session) is protected with the same basic auth approach as creating queues - the logic here is that opening private servers to all users as proxies would increase the scenarios for DoS attacks (which is the case with the public servers). + +The open question is whether the client should choose proxies from: +- all configured relays. +- there should be a subset of configured relays. +- there should be a separate list. + +E.g., there could be a second toggle in the relay configuration to allow using relay as proxy, in addition to the current toggle that allows creating queues. + +For simplicity, initially we will just use all enabled relays as potential proxies. + +3. How many proxying relays should be used during one session. + +This is not a simple question, and it creates a contradiction between two risks: +- collusion between proxies and destination relays simplifies correlating sending clients by session - from the point of view of this risk, clients should follow the same policy for creating connections with proxies, that is to create a new connection for each user profile, and if transport isolation is set to "per connection" - for each destination queue. +- traffic correlation by observable traffic sessions (particularly if an attacker can observe user's ISP traffic or multiple proxies) - from this point of view, it would be beneficial to use fewer proxies and fewer connections with proxies and see the risk of proxy colluding with the destination relay as lower than the risk of traffic observation that in the case of multiple sessions would allow to correlate traffic to rarely used destination relays (any private self-hosted relays) and the traffic of the user to a given proxy, to prove the fact of user communicating with the destination relay via the proxy. + +While we can transfer this choice on the users, it seems a complex decision to make, and overall the second risk (traffic correlation) seems more important to address than the first. + +In any case possible options are: +1. Extreme option 1: Create a new proxy session, with the new random proxy, for each potential transport session that would exist if the user were to be connected to destination relays directly. That is, never to mix access to multiple relays from multiple user profiles (and in case of per-connection isolation, to multiple queues) into a one client session with proxy. This is a rather radical option that nullifies any advantages of having fewer sessions with proxies than there would have been with the destination relays and removes any benefits of batching destination server session requests (PRXY comands). +2. Extreme option 2: Use only one proxy session at the time, mixing traffic from all user profiles and to all destination servers (and for all queues) into a session with one proxy. This minimizes the risks of traffic correlation in case of non-colluding proxy, but maximises the risk in case it colludes with the destination relays. +3. Balanced option: Use one proxy session per user profile, but mix traffic to multiple queues irrespective of connection isolation option and to all destination servers. Given that connection isolation is an experimental option, this makes the most sense, but it would have to be disclosed. +4. Less balanced option: take connection isolation option into account and create a new proxy connection for each destination queue. This feels worse than option 3. + +If option 3 is chosen, then the transport session key with the proxy would be different from the transport session key with the relay - proxy session will only use UserId as the key, and the relay session uses (UserId, Server, Maybe EntityId) as the key. + +If option 4 is chosen, the keys would also be different, as the proxy would then use (UserId, Maybe (Server, EntityId)) as the key. + +We could potentially key proxy sessions (and create proxy connections) per each destination relay, in the same way as we key relays themselves, but it seems to have the least sense, as we neither achieve isolation by queue in case proxy and destination relay collude, nor we sufficiently protect from traffic correlation by any observers. + +The implemented design is this: +- for each destination relay a random proxy is chosen and used to send all messages - all requests from a client coalesce to a single session. +- transport isolation mode is taken into account, that is if per-connection isolation is enabled, then a separate proxy connection will be created for each messaging queue. +- supported modes when proxy is used: always, for unknown relays, for unknown relays when IP address is not protected, never. + +This decision is made because the argument for protection against collusion between proxy and relay and more balanced traffic distribution is stronger than the argument for protection against traffic correlation, because even mixing all messages to one proxy connection does not provide protection against traffic correlation by time, so in any case it requires adding delays. + ### Threat model for SMP proxy and changes to threat model for SMP #### SMP proxy diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 2d9fedc65..600396700 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -149,7 +149,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Util (removePath) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Lock (withLock', withLock) +import Simplex.Messaging.Agent.Lock (withLock, withLock') import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval @@ -160,7 +160,7 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs) -import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOn, pattern PQEncOff, pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -198,7 +198,7 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = liftIO $ newSMPAgentEnv cfg store >>= runReaderT runAgent where runAgent = do - c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask + c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask t <- runAgentThreads c `forkFinally` const (liftIO $ disconnectAgentClient c) atomically . writeTVar acThread . Just =<< mkWeakThreadId t pure c @@ -239,7 +239,7 @@ createUser c = withAgentEnv c .: createUser' c {-# INLINE createUser #-} -- | Delete user record optionally deleting all user's connections on SMP servers -deleteUser :: AgentClient -> UserId -> Bool -> AE () +deleteUser :: AgentClient -> UserId -> Bool -> AE () deleteUser c = withAgentEnv c .: deleteUser' c {-# INLINE deleteUser #-} @@ -815,7 +815,7 @@ joinConnSrv c userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMo lift (compatibleContactUri cReqUri pqSup) >>= \case Just (qInfo, vrsn) -> do (connId', cReq) <- newConnSrv c userId connId enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv - sendInvitation c userId qInfo vrsn cReq cInfo + void $ sendInvitation c userId qInfo vrsn cReq cInfo pure connId' Nothing -> throwError $ AGENT A_VERSION @@ -1209,7 +1209,7 @@ enqueueMessage c cData sq msgFlags aMessage = {-# INLINE enqueueMessage #-} -- this function is used only for sending messages in batch, it returns the list of successes to enqueue additional deliveries -enqueueMessageB :: forall t. (Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) +enqueueMessageB :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) enqueueMessageB c reqs = do cfg <- asks config reqMids <- withStoreBatch c $ \db -> fmap (bindRight $ storeSentMsg db cfg) reqs @@ -1242,7 +1242,7 @@ enqueueSavedMessage :: AgentClient -> ConnData -> AgentMsgId -> SndQueue -> AM' enqueueSavedMessage c cData msgId sq = enqueueSavedMessageB c $ Identity (cData, [sq], msgId) {-# INLINE enqueueSavedMessage #-} -enqueueSavedMessageB :: (Foldable t) => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () +enqueueSavedMessageB :: Foldable t => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () enqueueSavedMessageB c reqs = do -- saving to the database is in the start to avoid race conditions when delivery is read from queue before it is saved void $ withStoreBatch' c $ \db -> concatMap (storeDeliveries db) reqs @@ -1333,7 +1333,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork retrySndMsg riMode = do withStore' c $ \db -> updatePendingMsgRIState db connId msgId riState retrySndOp c $ loop riMode - Right () -> do + Right proxySrv_ -> do case msgType of AM_CONN_INFO -> setConfirmed AM_CONN_INFO_REPLY -> setConfirmed @@ -1355,7 +1355,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork when (status == Active) $ notify $ CON pqEncryption -- this branch should never be reached as receive queue is created before the confirmation, _ -> logError "HELLO sent without receive queue" - AM_A_MSG_ -> notify $ SENT mId + AM_A_MSG_ -> notify $ SENT mId proxySrv_ AM_A_RCVD_ -> pure () AM_QCONT_ -> pure () AM_QADD_ -> pure () @@ -2212,7 +2212,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, where processEND = \case Just (Right clnt) - | sessId == sessionId (thParams clnt) -> do + | sessId == sessionId (thParams $ connectedClient clnt) -> do removeSubscription c connId notify' END pure "END" @@ -2574,7 +2574,7 @@ confirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do confirmQueue :: Compatible VersionSMPA -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () confirmQueue (Compatible agentVersion) c cData@ConnData {connId, pqSupport} sq srv connInfo e2eEncryption_ subMode = do msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode - sendConfirmation c sq msg + void $ sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed where mkConfirmation :: AgentMessage -> AM MsgBody diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 27223b12f..c29e35499 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -132,6 +132,8 @@ module Simplex.Messaging.Agent.Client SMPTransportSession, NtfTransportSession, XFTPTransportSession, + ProxiedRelay (..), + SMPConnectedClient (..), ) where @@ -230,7 +232,7 @@ import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (SMPVersion) -import Simplex.Messaging.Transport.Client (TransportHost) +import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version import System.Mem.Weak (Weak) @@ -263,6 +265,10 @@ data AgentClient = AgentClient msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg), smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), smpClients :: TMap SMPTransportSession SMPClientVar, + -- smpProxiedRelays: + -- SMPTransportSession defines connection from proxy to relay, + -- SMPServerWithAuth defines client connected to SMP proxy (with the same userId and entityId in TransportSession) + smpProxiedRelays :: TMap SMPTransportSession SMPServerWithAuth, ntfServers :: TVar [NtfServer], ntfClients :: TMap NtfTransportSession NtfClientVar, xftpServers :: TMap UserId (NonEmpty XFTPServerWithAuth), @@ -297,6 +303,13 @@ data AgentClient = AgentClient agentEnv :: Env } +data SMPConnectedClient = SMPConnectedClient + { connectedClient :: SMPClient, + proxiedRelays :: TMap SMPServer ProxiedRelayVar + } + +type ProxiedRelayVar = SessionVar (Either AgentErrorType ProxiedRelay) + getAgentWorker :: (Ord k, Show k) => String -> Bool -> AgentClient -> k -> TMap k Worker -> (Worker -> AM ()) -> AM' Worker getAgentWorker = getAgentWorker' id pure {-# INLINE getAgentWorker #-} @@ -428,6 +441,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = msgQ <- newTBQueue qSize smpServers <- newTVar smp smpClients <- TM.empty + smpProxiedRelays <- TM.empty ntfServers <- newTVar ntf ntfClients <- TM.empty xftpServers <- newTVar xftp @@ -463,6 +477,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = msgQ, smpServers, smpClients, + smpProxiedRelays, ntfServers, ntfClients, xftpServers, @@ -511,15 +526,19 @@ agentDRG AgentClient {agentEnv = Env {random}} = random class (Encoding err, Show err) => ProtocolServerClient v err msg | msg -> v, msg -> err where type Client msg = c | c -> msg getProtocolServerClient :: AgentClient -> TransportSession msg -> AM (Client msg) + type ProtoClient msg = c | c -> msg + protocolClient :: Client msg -> ProtoClient msg clientProtocolError :: err -> AgentErrorType - closeProtocolServerClient :: Client msg -> IO () - clientServer :: Client msg -> String - clientTransportHost :: Client msg -> TransportHost - clientSessionTs :: Client msg -> UTCTime + closeProtocolServerClient :: ProtoClient msg -> IO () + clientServer :: ProtoClient msg -> String + clientTransportHost :: ProtoClient msg -> TransportHost + clientSessionTs :: ProtoClient msg -> UTCTime instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where - type Client BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg + type Client BrokerMsg = SMPConnectedClient getProtocolServerClient = getSMPServerClient + type ProtoClient BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg + protocolClient = connectedClient clientProtocolError = SMP closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer @@ -529,6 +548,8 @@ instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where instance ProtocolServerClient NTFVersion ErrorType NtfResponse where type Client NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse getProtocolServerClient = getNtfServerClient + type ProtoClient NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse + protocolClient = id clientProtocolError = NTF closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer @@ -538,61 +559,120 @@ instance ProtocolServerClient NTFVersion ErrorType NtfResponse where instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where type Client FileResponse = XFTPClient getProtocolServerClient = getXFTPServerClient + type ProtoClient FileResponse = XFTPClient + protocolClient = id clientProtocolError = XFTP closeProtocolServerClient = X.closeXFTPClient clientServer = X.xftpClientServer clientTransportHost = X.xftpTransportHost clientSessionTs = X.xftpSessionTs -getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPClient -getSMPServerClient c@AgentClient {active, smpClients, msgQ, workerSeq} tSess@(userId, srv, _) = do +getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient +getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do unlessM (readTVarIO active) . throwError $ INACTIVE atomically (getSessVar workerSeq tSess smpClients) >>= either newClient (waitForProtocolClient c tSess) where - -- we resubscribe only on newClient error, but not on waitForProtocolClient error, - -- as the large number of delivery workers waiting for the client TMVar - -- make it expensive to check for pending subscriptions. - newClient v = - newProtocolClient c tSess smpClients connectClient v - `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e - connectClient :: SMPClientVar -> AM SMPClient - connectClient v = do + newClient v = do + prs <- atomically TM.empty + smpConnectClient c tSess prs v + +getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, ProxiedRelay) +getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do + unlessM (readTVarIO active) . throwError $ INACTIVE + proxySrv <- getNextServer c userId [destSrv] + atomically (getClientVar proxySrv) >>= \(tSess, auth, v) -> + either (newProxyClient tSess auth) (waitForProxyClient tSess auth) v + where + getClientVar :: SMPServerWithAuth -> STM (SMPTransportSession, Maybe SMP.BasicAuth, Either SMPClientVar SMPClientVar) + getClientVar proxySrv = do + ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure + let tSess = (userId, srv, qId) + (tSess,auth,) <$> getSessVar workerSeq tSess smpClients + newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + newProxyClient tSess auth v = do + (prs, rv) <- atomically $ do + prs <- TM.empty + -- we do not need to check if it is a new proxied relay session, + -- as the client is just created and there are no sessions yet + (prs,) . either id id <$> getSessVar workerSeq destSrv prs + clnt <- smpConnectClient c tSess prs v + (clnt,) <$> newProxiedRelay clnt auth rv + waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + waitForProxyClient tSess auth v = do + clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess v + sess <- + atomically (getSessVar workerSeq destSrv prs) + >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) + pure (clnt, sess) + newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM ProxiedRelay + newProxiedRelay clnt@(SMPConnectedClient smp prs) proxyAuth rv = + tryAgentError (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp destSrv proxyAuth) >>= \case + Right sess -> do + atomically $ putTMVar (sessionVar rv) (Right sess) + liftIO $ incClientStat c userId clnt "PROXY" "OK" + pure sess + Left e -> do + liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e + atomically $ do + removeSessVar rv destSrv prs + TM.delete destSess smpProxiedRelays + putTMVar (sessionVar rv) (Left e) + throwError e -- signal error to caller + waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM ProxiedRelay + waitForProxiedRelay (_, srv, _) rv = do + NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c + sess_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar rv) + liftEither $ case sess_ of + Just (Right sess) -> Right sess + Just (Left e) -> Left e + Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT + +smpConnectClient :: AgentClient -> SMPTransportSession -> TMap SMPServer ProxiedRelayVar -> SMPClientVar -> AM SMPConnectedClient +smpConnectClient c@AgentClient {smpClients, msgQ} tSess@(_, srv, _) prs v = + newProtocolClient c tSess smpClients connectClient v + `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e + where + connectClient :: SMPClientVar -> AM SMPConnectedClient + connectClient v' = do cfg <- lift $ getClientConfig c smpCfg g <- asks random env <- ask - liftError' (protocolClientError SMP $ B.unpack $ strEncode srv) $ - getProtocolClient g tSess cfg (Just msgQ) $ - clientDisconnected env v + liftError (protocolClientError SMP $ B.unpack $ strEncode srv) $ do + smp <- ExceptT $ getProtocolClient g tSess cfg (Just msgQ) $ smpClientDisconnected c tSess env v' prs + pure SMPConnectedClient {connectedClient = smp, proxiedRelays = prs} - clientDisconnected :: Env -> SMPClientVar -> SMPClient -> IO () - clientDisconnected env v client = do - removeClientAndSubs >>= serverDown - logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv +smpClientDisconnected :: AgentClient -> SMPTransportSession -> Env -> SMPClientVar -> TMap SMPServer ProxiedRelayVar -> SMPClient -> IO () +smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess@(userId, srv, qId) env v prs client = do + removeClientAndSubs >>= serverDown + logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv + where + -- we make active subscriptions pending only if the client for tSess was current (in the map) and active, + -- because we can have a race condition when a new current client could have already + -- made subscriptions active, and the old client would be processing diconnection later. + removeClientAndSubs :: IO ([RcvQueue], [ConnId]) + removeClientAndSubs = atomically $ ifM currentActiveClient removeSubs $ pure ([], []) where - -- we make active subscriptions pending only if the client for tSess was current (in the map) and active, - -- because we can have a race condition when a new current client could have already - -- made subscriptions active, and the old client would be processing diconnection later. - removeClientAndSubs :: IO ([RcvQueue], [ConnId]) - removeClientAndSubs = atomically $ ifM currentActiveClient removeSubs $ pure ([], []) - where - currentActiveClient = (&&) <$> removeSessVar' v tSess smpClients <*> readTVar active - removeSubs = do - (qs, cs) <- RQ.getDelSessQueues tSess $ activeSubs c - RQ.batchAddQueues (pendingSubs c) qs - pure (qs, cs) + currentActiveClient = (&&) <$> removeSessVar' v tSess smpClients <*> readTVar active + removeSubs = do + (qs, cs) <- RQ.getDelSessQueues tSess $ activeSubs c + RQ.batchAddQueues (pendingSubs c) qs + -- this removes proxied relays that this client created sessions to + destSrvs <- M.keys <$> readTVar prs + forM_ destSrvs $ \destSrv -> TM.delete (userId, destSrv, qId) smpProxiedRelays + pure (qs, cs) - serverDown :: ([RcvQueue], [ConnId]) -> IO () - serverDown (qs, conns) = whenM (readTVarIO active) $ do - incClientStat c userId client "DISCONNECT" "" - notifySub "" $ hostEvent DISCONNECT client - unless (null conns) $ notifySub "" $ DOWN srv conns - unless (null qs) $ do - atomically $ mapM_ (releaseGetLock c) qs - runReaderT (resubscribeSMPSession c tSess) env + serverDown :: ([RcvQueue], [ConnId]) -> IO () + serverDown (qs, conns) = whenM (readTVarIO active) $ do + incClientStat' c userId client "DISCONNECT" "" + notifySub "" $ hostEvent' DISCONNECT client + unless (null conns) $ notifySub "" $ DOWN srv conns + unless (null qs) $ do + atomically $ mapM_ (releaseGetLock c) qs + runReaderT (resubscribeSMPSession c tSess) env - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () - notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) + notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () + notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) resubscribeSMPSession :: AgentClient -> SMPTransportSession -> AM' () resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = @@ -735,7 +815,11 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = throwError e -- signal error to caller hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> Client msg -> ACommand 'Agent 'AENone -hostEvent event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost +hostEvent event = hostEvent' event . protocolClient +{-# INLINE hostEvent #-} + +hostEvent' :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> ProtoClient msg -> ACommand 'Agent 'AENone +hostEvent' event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost getClientConfig :: AgentClient -> (AgentConfig -> ProtocolClientConfig v) -> AM' (ProtocolClientConfig v) getClientConfig c cfgSel = do @@ -842,7 +926,7 @@ closeClient_ c v = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c E.handle (\BlockedIndefinitelyOnSTM -> pure ()) $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) >>= \case - Just (Right client) -> closeProtocolServerClient client `catchAll_` pure () + Just (Right client) -> closeProtocolServerClient (protocolClient client) `catchAll_` pure () _ -> pure () closeXFTPServerClient :: AgentClient -> UserId -> XFTPServer -> FileDigest -> IO () @@ -895,6 +979,22 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do stat cl $ strEncode e throwError e +withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a +withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do + cp@(cl, _) <- getSMPProxyClient c destSess + logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr + r <- (action cp <* stat cl "OK") `catchAgentError` logServerError cl + logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" + pure r + where + stat cl = liftIO . incClientStat c userId cl cmdStr + proxySrv = showServer . protocolClientServer' . protocolClient + logServerError :: SMPConnectedClient -> AgentErrorType -> AM a + logServerError cl e = do + logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ strEncode e + stat cl $ strEncode e + throwError e + withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> AM a) -> AM a withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do logServer "-->" c srv entId cmdStr @@ -903,22 +1003,46 @@ withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do return res withClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer client) $ action client +withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withClient #-} withLogClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withLogClient c tSess entId cmdStr action = withLogClient_ c tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer client) $ action client +withLogClient c tSess entId cmdStr action = withLogClient_ c tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withLogClient #-} withSMPClient :: SMPQueueRec q => AgentClient -> q -> ByteString -> (SMPClient -> ExceptT SMPClientError IO a) -> AM a withSMPClient c q cmdStr action = do tSess <- liftIO $ mkSMPTransportSession c q - withLogClient c tSess (queueId q) cmdStr action + withLogClient c tSess (queueId q) cmdStr $ action . connectedClient -withSMPClient_ :: SMPQueueRec q => AgentClient -> q -> ByteString -> (SMPClient -> AM a) -> AM a -withSMPClient_ c q cmdStr action = do - tSess <- liftIO $ mkSMPTransportSession c q - withLogClient_ c tSess (queueId q) cmdStr action +sendOrProxySMPMessage :: AgentClient -> UserId -> SMPServer -> ByteString -> Maybe SMP.SndPrivateAuthKey -> SMP.SenderId -> MsgFlags -> SMP.MsgBody -> AM (Maybe SMPServer) +sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do + sess <- liftIO $ mkTransportSession c userId destSrv senderId + ifM (atomically shouldUseProxy) (sendViaProxy sess) (sendDirectly sess $> Nothing) + where + shouldUseProxy = do + cfg <- getNetworkConfig c + case smpProxyMode cfg of + SPMAlways -> pure True + SPMUnknown -> unknownServer + SPMUnprotected + | ipAddressProtected cfg destSrv -> pure False + | otherwise -> unknownServer + SPMNever -> pure False + unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) + sendViaProxy destSess = + withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do + liftClient SMP (clientServer smp) $ proxySMPMessage smp proxySess spKey_ senderId msgFlags msg + pure . Just $ protocolClientServer' smp + sendDirectly tSess = + withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> + liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg + +ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool +ipAddressProtected NetworkConfig {socksProxy, hostMode} (ProtocolServer _ hosts _ _) = do + isJust socksProxy || (hostMode == HMOnion && any isOnionHost hosts) + where + isOnionHost = \case THOnionHost _ -> True; _ -> False withNtfClient :: AgentClient -> NtfServer -> EntityId -> ByteString -> (NtfClient -> ExceptT NtfClientError IO a) -> AM a withNtfClient c srv = withLogClient c (0, srv, Nothing) @@ -989,7 +1113,6 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do liftError (testErr TSSecureQueue) $ secureSMPQueue smp rpKey rcvId sKey liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp rpKey rcvId ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient smp - incClientStat c userId smp "SMP_TEST" "OK" pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok Left e -> pure (Just $ testErr TSConnect e) where @@ -1104,7 +1227,7 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do logServer "-->" c srv "" "NEW" tSess <- liftIO $ mkTransportSession c userId srv connId QIK {rcvId, sndId, rcvPublicDhKey} <- - withClient c tSess "NEW" $ \smp -> createSMPQueue smp rKeys dhKey auth subMode + withClient c tSess "NEW" $ \smp -> createSMPQueue (connectedClient smp) rKeys dhKey auth subMode liftIO . logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = RcvQueue @@ -1193,7 +1316,7 @@ sendTSessionBatches statCmd statBatchSize toRQ action c qs = sendClientBatch (tSess@(userId, srv, _), qs') = tryAgentError' (getSMPServerClient c tSess) >>= \case Left e -> pure $ L.map ((,Left e) . toRQ) qs' - Right smp -> liftIO $ do + Right (SMPConnectedClient smp _) -> liftIO $ do logServer "-->" c srv (bshow (length qs') <> " queues") statCmd rs <- L.map agentError <$> action smp qs' statBatch @@ -1243,20 +1366,17 @@ logSecret :: ByteString -> ByteString logSecret bs = encode $ B.take 3 bs {-# INLINE logSecret #-} -sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM () -sendConfirmation c sq@SndQueue {sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = - withSMPClient_ c sq "SEND " $ \smp -> do - let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation - msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg - liftClient SMP (clientServer smp) $ sendSMPMessage smp Nothing sndId (SMP.MsgFlags {notification = True}) msg +sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM (Maybe SMPServer) +sendConfirmation c sq@SndQueue {userId, server, sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do + let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation + msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg + sendOrProxySMPMessage c userId server "" Nothing sndId (MsgFlags {notification = True}) msg sendConfirmation _ _ _ = throwError $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database" -sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM () +sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM (Maybe SMPServer) sendInvitation c userId (Compatible (SMPQueueInfo v SMPQueueAddress {smpServer, senderId, dhPublicKey})) (Compatible agentVersion) connReq connInfo = do - tSess <- liftIO $ mkTransportSession c userId smpServer senderId - withLogClient_ c tSess senderId "SEND " $ \smp -> do - msg <- mkInvitation - liftClient SMP (clientServer smp) $ sendSMPMessage smp Nothing senderId MsgFlags {notification = True} msg + msg <- mkInvitation + sendOrProxySMPMessage c userId smpServer "" Nothing senderId (MsgFlags {notification = True}) msg where mkInvitation :: AM ByteString -- this is only encrypted with per-queue E2E, not with double ratchet @@ -1340,12 +1460,11 @@ deleteQueue c rq@RcvQueue {rcvId, rcvPrivateKey} = do deleteQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] deleteQueues = sendTSessionBatches "DEL" 90 id $ sendBatch deleteSMPQueues -sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM () -sendAgentMessage c sq@SndQueue {sndId, sndPrivateKey} msgFlags agentMsg = - withSMPClient_ c sq "SEND " $ \smp -> do - let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg - msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg - liftClient SMP (clientServer smp) $ sendSMPMessage smp (Just sndPrivateKey) sndId msgFlags msg +sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM (Maybe SMPServer) +sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags agentMsg = do + let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg + msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg + sendOrProxySMPMessage c userId server "" (Just sndPrivateKey) sndId msgFlags msg agentNtfRegisterToken :: AgentClient -> NtfToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519) agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey = @@ -1606,9 +1725,13 @@ incStat AgentClient {agentStats} n k = do _ -> newTVar n >>= \v -> TM.insert k v agentStats incClientStat :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> ByteString -> ByteString -> IO () -incClientStat c userId pc = incClientStatN c userId pc 1 +incClientStat c userId = incClientStat' c userId . protocolClient {-# INLINE incClientStat #-} +incClientStat' :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> ByteString -> ByteString -> IO () +incClientStat' c userId pc = incClientStatN c userId pc 1 +{-# INLINE incClientStat' #-} + incServerStat :: AgentClient -> UserId -> ProtocolServer p -> ByteString -> ByteString -> IO () incServerStat c userId ProtocolServer {host} cmd res = do threadDelay 100000 @@ -1616,7 +1739,7 @@ incServerStat c userId ProtocolServer {host} cmd res = do where statsKey = AgentStatsKey {userId, host = strEncode $ L.head host, clientTs = "", cmd, res} -incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> Int -> ByteString -> ByteString -> IO () +incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> Int -> ByteString -> ByteString -> IO () incClientStatN c userId pc n cmd res = do atomically $ incStat c n statsKey where diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 4ee8d373f..602ffafe4 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -193,13 +193,13 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), PQEncryption (..), - pattern PQEncOff, PQSupport, - pattern PQSupportOn, - pattern PQSupportOff, RcvE2ERatchetParams, RcvE2ERatchetParamsUri, - SndE2ERatchetParams + SndE2ERatchetParams, + pattern PQEncOff, + pattern PQSupportOff, + pattern PQSupportOn, ) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -213,14 +213,14 @@ import Simplex.Messaging.Protocol MsgId, NMsgMeta, ProtocolServer (..), + SMPClientVersion, SMPMsgMeta, SMPServer, SMPServerWithAuth, SndPublicAuthKey, SubscriptionMode, - SMPClientVersion, - VersionSMPC, VersionRangeSMPC, + VersionSMPC, initialSMPClientVersion, legacyEncodeServer, legacyServerP, @@ -398,7 +398,7 @@ data ACommand (p :: AParty) (e :: AEntity) where RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> ACommand Agent AEConn SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn - SENT :: AgentMsgId -> ACommand Agent AEConn + SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn @@ -517,7 +517,7 @@ aCommandTag = \case RSYNC {} -> RSYNC_ SEND {} -> SEND_ MID {} -> MID_ - SENT _ -> SENT_ + SENT {} -> SENT_ MERR {} -> MERR_ MERRS {} -> MERRS_ MSG {} -> MSG_ @@ -913,7 +913,7 @@ instance Encoding AgentMsgEnvelope where -- AgentRatchetInfo is not encrypted with double ratchet, but with per-queue E2E encryption data AgentMessage = -- used by the initiating party when confirming reply queue - AgentConnInfo ConnInfo + AgentConnInfo ConnInfo | -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation. -- It made removed REPLY message unnecessary. AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo @@ -1387,9 +1387,9 @@ deriving instance Show (ConnectionRequestUri m) data AConnectionRequestUri = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequestUri m) instance Eq AConnectionRequestUri where - ACR m cr == ACR m' cr' = case testEquality m m' of - Just Refl -> cr == cr' - _ -> False + ACR m cr == ACR m' cr' = case testEquality m m' of + Just Refl -> cr == cr' + _ -> False deriving instance Show AConnectionRequestUri @@ -1793,7 +1793,7 @@ commandP binaryP = SWITCH_ -> s (SWITCH <$> strP_ <*> strP_ <*> strP) RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP) MID_ -> s (MID <$> A.decimal <*> _strP) - SENT_ -> s (SENT <$> A.decimal) + SENT_ -> s (SENT <$> A.decimal <*> _strP) MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP) MERRS_ -> s (MERRS <$> strP_ <*> strP) MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP) @@ -1856,7 +1856,7 @@ serializeCommand = \case RSYNC rrState cryptoErr cstats -> s (RSYNC_, rrState, cryptoErr, cstats) SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody] MID mId pqEnc -> s (MID_, mId, pqEnc) - SENT mId -> s (SENT_, mId) + SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_) MERR mId e -> s (MERR_, mId, e) MERRS mIds e -> s (MERRS_, mIds, e) MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody] diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 8d4db29d9..ebcfc79b2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -30,9 +30,11 @@ module Simplex.Messaging.Client TransportSession, ProtocolClient (thParams, sessionTs), SMPClient, + ProxiedRelay (..), getProtocolClient, closeProtocolClient, protocolClientServer, + protocolClientServer', transportHost', transportSession', @@ -54,7 +56,7 @@ module Simplex.Messaging.Client suspendSMPQueue, deleteSMPQueue, deleteSMPQueues, - createSMPProxySession, + connectSMPProxiedRelay, proxySMPMessage, forwardSMPMessage, sendProtocolCommand, @@ -65,6 +67,8 @@ module Simplex.Messaging.Client ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), + HostMode (..), + SMPProxyMode (..), defaultClientConfig, defaultSMPClientConfig, defaultNetworkConfig, @@ -207,6 +211,8 @@ data NetworkConfig = NetworkConfig requiredHostMode :: Bool, -- | transport sessions are created per user or per entity sessionMode :: TransportSessionMode, + -- | SMP proxy mode + smpProxyMode :: SMPProxyMode, -- | timeout for the initial client TCP/TLS connection (microseconds) tcpConnectTimeout :: Int, -- | timeout of protocol commands (microseconds) @@ -226,6 +232,14 @@ data NetworkConfig = NetworkConfig data TransportSessionMode = TSMUser | TSMEntity deriving (Eq, Show) +-- SMP proxy mode for sending messages +data SMPProxyMode + = SPMAlways + | SPMUnknown -- use with unknown relays + | SPMUnprotected -- use with unknown relays when IP address is not protected (i.e., when neither SOCKS proxy nor .onion address is used) + | SPMNever + deriving (Eq, Show) + defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig @@ -233,6 +247,7 @@ defaultNetworkConfig = hostMode = HMOnionViaSocks, requiredHostMode = False, sessionMode = TSMUser, + smpProxyMode = SPMNever, tcpConnectTimeout = 20_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 5_000, @@ -302,10 +317,14 @@ chooseTransportHost NetworkConfig {socksProxy, hostMode, requiredHostMode} hosts publicHost = find (not . isOnionHost) hosts protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient v err msg -> String -protocolClientServer = B.unpack . strEncode . snd3 . transportSession . client_ +protocolClientServer = B.unpack . strEncode . protocolClientServer' +{-# INLINE protocolClientServer #-} + +protocolClientServer' :: ProtocolClient v err msg -> ProtoServer msg +protocolClientServer' = snd3 . transportSession . client_ where snd3 (_, s, _) = s -{-# INLINE protocolClientServer #-} +{-# INLINE protocolClientServer' #-} transportHost' :: ProtocolClient v err msg -> TransportHost transportHost' = transportHost . client_ @@ -650,14 +669,13 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg -createSMPProxySession :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO (SessionId, VersionSMP, C.PublicKeyX25519) -createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = +connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay +connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - -- XXX: rfc says sessionId should be in the entityId of response PKEY sId vr (chain, key) -> do case supportedClientSMPRelayVRange `compatibleVersion` vr of Nothing -> throwE PCEIncompatibleHost -- TODO different error - Just (Compatible v) -> liftEitherWith x509Error $ (sId,v,) <$> validateRelay chain key + Just (Compatible v) -> liftEitherWith x509Error $ ProxiedRelay sId v <$> validateRelay chain key r -> throwE . PCEUnexpectedResponse $ bshow r where x509Error :: String -> SMPClientError @@ -672,6 +690,12 @@ createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyA pubKey <- C.verifyX509 serverKey exact C.x509ToPublic (pubKey, []) >>= C.pubKey +data ProxiedRelay = ProxiedRelay + { prSessionId :: SessionId, + prVersion :: VersionSMP, + prServerKey :: C.PublicKeyX25519 + } + -- consider how to process slow responses - is it handled somehow locally or delegated to the caller -- this method is used in the client -- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender @@ -679,9 +703,7 @@ createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyA proxySMPMessage :: SMPClient -> -- proxy session from PKEY - SessionId -> - VersionSMP -> - C.PublicKeyX25519 -> + ProxiedRelay -> -- message to deliver Maybe SndPrivateAuthKey -> SenderId -> @@ -689,7 +711,7 @@ proxySMPMessage :: MsgBody -> ExceptT SMPClientError IO () -- TODO use version -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} sessionId _v serverKey spKey sId flags msg = do +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams serverThParams = proxyThParams {sessionId, thAuth = serverThAuth} @@ -867,4 +889,6 @@ $(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode) $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) + $(J.deriveJSON defaultJSON ''NetworkConfig) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 8083ef988..b14917c18 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -13,7 +13,7 @@ module AgentTests (agentTests) where import AgentTests.ConnectionRequestTests import AgentTests.DoubleRatchetTests (doubleRatchetTests) -import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg') +import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg', pattern SENT) import AgentTests.MigrationTests (migrationTests) import AgentTests.NotificationTests (notificationTests) import AgentTests.SQLiteTests (storeTests) @@ -27,7 +27,7 @@ import GHC.Stack (withFrozenCallStack) import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) -import Simplex.Messaging.Agent.Protocol hiding (MID, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (MID, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOn, pattern IKPQOff, pattern PQEncOn, pattern PQSupportOn, pattern PQSupportOff) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -437,8 +437,8 @@ testServerConnectionAfterError t _ = do bob #: ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT alice #: ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT withServer $ do - alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False - alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False + alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False + alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 301be97b4..13822650f 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -45,6 +45,7 @@ module AgentTests.FunctionalAPITests pattern REQ, pattern Msg, pattern Msg', + pattern SENT, agentCfgV7, ) where @@ -70,17 +71,17 @@ import Data.Word (Word16) import qualified Database.SQLite.Simple as SQL import GHC.Stack (withFrozenCallStack) import SMPAgentClient -import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) +import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import qualified Simplex.Messaging.Agent as A import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore) -import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -171,6 +172,9 @@ pattern MsgErr msgId err msgBody <- MSG MsgMeta {recipient = (msgId, _), integri pattern MsgErr' :: AgentMsgId -> MsgErrorType -> PQEncryption -> MsgBody -> ACommand 'Agent 'AEConn pattern MsgErr' msgId err pq msgBody <- MSG MsgMeta {recipient = (msgId, _), integrity = MsgError err, pqEncryption = pq} _ msgBody +pattern SENT :: AgentMsgId -> ACommand 'Agent 'AEConn +pattern SENT msgId = A.SENT msgId Nothing + pattern Rcvd :: AgentMsgId -> ACommand 'Agent 'AEConn pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMsgId, msgRcptStatus = MROk}] @@ -448,26 +452,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = let v = basicAuthSMPVersion in allowNew && (isNothing srvAuth || (srvVersion >= v && clntVersion >= v && srvAuth == clntAuth)) -testMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn - it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn - it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn - it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff - it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff - it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False + it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False + it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False + it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False + it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False + it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False -testRatchetMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn - it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn - it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn - it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff - it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff - it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False + it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False + it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False + it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff False + it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff False + it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff False testServerMatrix2 :: ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -475,10 +481,14 @@ testServerMatrix2 t runTest = do it "2 servers" $ withSmpServer t . withSmpServerOn t testPort2 $ runTest initAgentServers2 runTestCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () -runTestCfg2 aCfg bCfg baseMsgId runTest = - withAgentClientsCfg2 aCfg bCfg $ \a b -> runTest a b baseMsgId +runTestCfg2 aCfg bCfg = runTestCfgServers2 aCfg bCfg initAgentServers {-# INLINE runTestCfg2 #-} +runTestCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () +runTestCfgServers2 aCfg bCfg servers baseMsgId runTest = + withAgentClientsCfgServers2 aCfg bCfg servers $ \a b -> runTest a b baseMsgId +{-# INLINE runTestCfgServers2 #-} + withAgentClientsCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO () withAgentClientsCfgServers2 aCfg bCfg servers runTest = withAgent 1 aCfg servers testDB $ \a -> @@ -499,8 +509,8 @@ withAgentClients3 runTest = withAgent 3 agentCfg initAgentServers testDB3 $ \c -> runTest a b c -runAgentClientTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientTest pqSupport alice@AgentClient {} bob baseId = +runAgentClientTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientTest pqSupport viaProxy alice@AgentClient {} bob baseId = runRight_ $ do (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqSupport) SMSubscribe aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe @@ -512,18 +522,19 @@ runAgentClientTest pqSupport alice@AgentClient {} bob baseId = get bob ##> ("", aliceId, A.INFO pqSupport "alice's connInfo") get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let proxySrv = if viaProxy then Just testSMPServer else Nothing 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT $ baseId + 1) + get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv) 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" - get alice ##> ("", bobId, SENT $ baseId + 2) + get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv) get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" - get bob ##> ("", aliceId, SENT $ baseId + 3) + get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv) 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" - get bob ##> ("", aliceId, SENT $ baseId + 4) + get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv) get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False @@ -627,8 +638,8 @@ testAgentClient3 = get c =##> \case ("", connId, Msg "c5") -> connId == aIdForC; _ -> False ackMessage c aIdForC 5 Nothing -runAgentClientContactTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientContactTest pqSupport alice bob baseId = +runAgentClientContactTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientContactTest pqSupport viaProxy alice bob baseId = runRight_ $ do (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqSupport) SMSubscribe aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe @@ -643,18 +654,19 @@ runAgentClientContactTest pqSupport alice bob baseId = get alice ##> ("", bobId, A.CON pqEnc) get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let proxySrv = if viaProxy then Just testSMPServer else Nothing 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT $ baseId + 1) + get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv) 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" - get alice ##> ("", bobId, SENT $ baseId + 2) + get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv) get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" - get bob ##> ("", aliceId, SENT $ baseId + 3) + get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv) 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" - get bob ##> ("", aliceId, SENT $ baseId + 4) + get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv) get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False @@ -1493,9 +1505,9 @@ testSuspendingAgentCompleteSending t = withAgentClients2 $ \a b -> do liftIO $ suspendAgent b 5000000 withSmpServerStoreLogOn t testPort $ \_ -> runRight_ @AgentErrorType $ do - pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC _ (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False ("", "", SUSPENDED) <- nGet b pGet a =##> \case ("", c, APC _ (Msg "hello too")) -> c == bId; ("", "", APC _ UP {}) -> True; _ -> False diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 2c1045791..b4f6ec3ee 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.NotificationTests where @@ -17,10 +17,6 @@ import AgentTests.FunctionalAPITests createConnection, exchangeGreetingsMsgId, get, - withAgent, - withAgentClients2, - withAgentClientsCfgServers2, - withAgentClients3, joinConnection, makeConnection, nGet, @@ -29,13 +25,18 @@ import AgentTests.FunctionalAPITests sendMessage, switchComplete, testServerMatrix2, + withAgent, + withAgentClients2, + withAgentClients3, withAgentClientsCfg2, + withAgentClientsCfgServers2, (##>), (=##>), pattern CON, pattern CONF, pattern INFO, pattern Msg, + pattern SENT, ) import Control.Concurrent (ThreadId, killThread, threadDelay) import Control.Monad @@ -55,12 +56,12 @@ import SMPClient (cfg, cfgV7, testPort, testPort2, testStoreLogFile2, withSmpSer import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore') import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers) -import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT) import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Protocol +import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Types (NtfToken (..)) import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) @@ -151,7 +152,8 @@ testNtfMatrix t runTest = do it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest skip "this case cannot be supported - see RFC" $ - it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest + it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ + runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest -- servers can be migrated in any order it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest @@ -258,7 +260,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do atomically $ readTBQueue apnsQ liftIO $ sendApnsResponse APNSRespOk pure ntfData - -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server + -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server threadDelay 1000000 withAgent 2 agentCfg initAgentServers testDB $ \a' -> -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 59370e654..d509042f0 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -20,7 +20,8 @@ import qualified Database.SQLite.Simple as SQL import Network.Socket (ServiceName) import NtfClient (ntfTestPort) import SMPClient - ( serverBracket, + ( proxyVRange, + serverBracket, testKeyHash, testPort, testPort2, @@ -34,7 +35,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultSMPClientConfig, defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyMode, chooseTransportHost, defaultSMPClientConfig, defaultNetworkConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) @@ -198,6 +199,9 @@ initAgentServers = initAgentServers2 :: InitialAgentServers initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]} +initAgentServersProxy :: SMPProxyMode -> InitialAgentServers +initAgentServersProxy smpProxyMode = initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode}} + agentCfg :: AgentConfig agentCfg = defaultAgentConfig @@ -217,6 +221,9 @@ agentCfg = where networkConfig = defaultNetworkConfig {tcpConnectTimeout = 3_000_000, tcpTimeout = 2_000_000} +agentProxyCfg :: AgentConfig +agentProxyCfg = agentCfg {smpCfg = (smpCfg agentCfg) {serverVRange = proxyVRange}} + fastRetryInterval :: RetryInterval fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index ad4d00266..e27970608 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -123,9 +123,12 @@ proxyCfg = cfgV7 { allowSMPProxy = True, smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, agreeSecret = True}} + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}} } +proxyVRange :: VersionRangeSMP +proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion + withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} @@ -163,6 +166,9 @@ withSmpServer t = withSmpServerOn t testPort withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const +withSmpServerProxy :: HasCallStack => ATransport -> IO a -> IO a +withSmpServerProxy t = withSmpServerConfigOn t proxyCfg testPort . const + runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a runSmpTest test = withSmpServer (transport @c) $ testSMPClient test diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 2a33ec055..ae2a05e4d 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -1,8 +1,11 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -10,15 +13,23 @@ module SMPProxyTests where -import AgentTests.FunctionalAPITests (runRight_) +import AgentTests.FunctionalAPITests import Data.ByteString.Char8 (ByteString) -import SMPAgentClient (testSMPServer, testSMPServer2) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import SMPAgentClient import SMPClient -import qualified SMPClient as SMP import ServerTests (decryptMsgV3, sendRecv) +import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) +import qualified Simplex.Messaging.Agent as A +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..)) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) +import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Protocol +import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOn) +import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) @@ -37,49 +48,52 @@ smpProxyTests = do xit "no SMP service at host/port" todo xit "bad SMP fingerprint" todo xit "batching proxy requests" todo - describe "forwarding requests" $ do - describe "deliver message via SMP proxy" $ do + describe "deliver message via SMP proxy" $ do + let srv1 = SMPServer testHost testPort testKeyHash + srv2 = SMPServer testHost testPort2 testKeyHash + describe "client API" $ do let maxLen = maxMessageLength sendingProxySMPVersion - it "same server" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = proxyServ + describe "one server" $ do + it "deliver via proxy" . oneServer $ do + deliverMessageViaProxy srv1 srv1 C.SEd448 "hello 1" "hello 2" + describe "two servers" $ do + let proxyServ = srv1 + relayServ = srv2 + (msg1, msg2) <- runIO $ do + g <- C.newRandom + atomically $ (,) <$> C.randomBytes maxLen g <*> C.randomBytes maxLen g + it "deliver via proxy" . twoServersFirstProxy $ deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" - it "different servers" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" - it "max message size, Ed448 keys" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - g <- C.newRandom - msg <- atomically $ C.randomBytes maxLen g - msg' <- atomically $ C.randomBytes maxLen g - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg' - it "max message size, Ed25519 keys" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - g <- C.newRandom - msg <- atomically $ C.randomBytes maxLen g - msg' <- atomically $ C.randomBytes maxLen g - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg' - it "max message size, X25519 keys" $ - withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> - withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do - g <- C.newRandom - msg <- atomically $ C.randomBytes maxLen g - msg' <- atomically $ C.randomBytes maxLen g - let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash - let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash - deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg' - xit "sender-proxy-relay-recipient works" todo - xit "similar timing for proxied and direct sends" todo + it "max message size, Ed448 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd448 msg1 msg2 + it "max message size, Ed25519 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg1 msg2 + it "max message size, X25519 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SX25519 msg1 msg2 + describe "agent API" $ do + describe "one server" $ do + it "always via proxy" . oneServer $ + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + it "without proxy" . oneServer $ + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" + describe "two servers" $ do + it "always via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + it "both via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" + it "first via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + it "without proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + it "first via proxy for unknown" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + where + oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg testPort . const + twoServers = twoServers_ proxyCfg proxyCfg + twoServersFirstProxy = twoServers_ proxyCfg cfgV7 + twoServers_ cfg1 cfg2 runTest = + withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO () deliverMessageViaProxy proxyServ relayServ alg msg msg' = do @@ -97,39 +111,74 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv -- get proxy session - (sessId, v, relayKey) <- createSMPProxySession pc relayServ (Just "correct") + sess <- connectSMPProxiedRelay pc relayServ (Just "correct") -- send via proxy to unsecured queue - proxySMPMessage pc sessId v relayKey Nothing sndId noMsgFlags msg + proxySMPMessage pc sess Nothing sndId noMsgFlags msg -- receive 1 - (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ + (_tSess, _v, _sid, _ety, SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId encBody `shouldBe` Right msg ackSMPMessage rc rPriv rcvId msgId -- secure queue (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g secureSMPQueue rc rPriv rcvId sPub -- send via proxy to secured queue - proxySMPMessage pc sessId v relayKey (Just sPriv) sndId noMsgFlags msg' + proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' -- receive 2 - (_tSess, _v, _sid, _ety, MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ + (_tSess, _v, _sid, _ety, SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId' encBody' `shouldBe` Right msg' ackSMPMessage rc rPriv rcvId msgId' -proxyVRange :: VersionRangeSMP -proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion +agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> IO () +agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 = + withAgent 1 aCfg (servers aTestCfg) testDB $ \alice -> + withAgent 2 aCfg (servers bTestCfg) testDB2 $ \bob -> runRight_ $ do + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" PQSupportOn SMSubscribe + ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice + liftIO $ pqSup' `shouldBe` PQSupportOn + allowConnection alice bobId confId "alice's connInfo" + let pqEnc = CR.PQEncOn + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let aProxySrv = if aViaProxy then Just $ L.head aSrvs else Nothing + 1 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + get alice ##> ("", bobId, A.SENT (baseId + 1) aProxySrv) + 2 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg2 + get alice ##> ("", bobId, A.SENT (baseId + 2) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 1) Nothing + get bob =##> \case ("", c, Msg' _ pq msg2') -> c == aliceId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage bob aliceId (baseId + 2) Nothing + let bProxySrv = if bViaProxy then Just $ L.head bSrvs else Nothing + 3 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg1 + get bob ##> ("", aliceId, A.SENT (baseId + 3) bProxySrv) + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg1') -> c == bobId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage alice bobId (baseId + 3) Nothing + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 4) Nothing + where + baseId = 3 + msgId = subtract baseId . fst + aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} + servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode) {smp = userServers $ L.map noAuthSrv srvs} testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) - reply `shouldBe` Right (ERR AUTH) + reply `shouldBe` Right (SMP.ERR SMP.AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - reply `shouldBe` Right (ERR AUTH) + reply `shouldBe` Right (SMP.ERR SMP.AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} From 6f832733189f769e195e8b3951e28d88b8118ec4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 2 May 2024 15:14:01 +0100 Subject: [PATCH 007/125] client: increase timeout for SOCKS connection, increase timeout for direct connection (#1123) --- src/Simplex/Messaging/Transport/Client.hs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Messaging/Transport/Client.hs b/src/Simplex/Messaging/Transport/Client.hs index 08cff1d0d..a943b36d9 100644 --- a/src/Simplex/Messaging/Transport/Client.hs +++ b/src/Simplex/Messaging/Transport/Client.hs @@ -19,7 +19,7 @@ module Simplex.Messaging.Transport.Client TransportHost (..), TransportHosts (..), TransportHosts_ (..), - validateCertificateChain + validateCertificateChain, ) where @@ -52,7 +52,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll, parseString) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.KeepAlive -import Simplex.Messaging.Util (bshow, (<$?>), catchAll, tshow) +import Simplex.Messaging.Util (bshow, catchAll, tshow, (<$?>)) import System.IO.Error import System.Timeout (timeout) import Text.Read (readMaybe) @@ -143,14 +143,19 @@ runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, serverCert <- newEmptyTMVarIO let hostName = B.unpack $ strEncode host clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert - connectTCP = case socksProxy of - Just proxy -> connectSocksClient proxy proxyUsername $ hostAddr host - _ -> connectTCPClient hostName + (connectTCP, tlsTimeout) = case socksProxy of + -- We use a much larger timeout for connections via SOCKS proxy, to allow the circuits created + -- in the socket connection that would otherwise timeout to be used in the next connection attempt. + -- Using standard timeout results in permanent timeout for the clients using SOCKS in cases + -- when SOCKS proxy is very slow (bad network, congestion in underlying network, etc.), + -- because SOCKS proxy destroys circuits when the last session using them is closed. + Just proxy -> (connectSocksClient proxy proxyUsername (hostAddr host), tcpConnectTimeout * 10) + _ -> (connectTCPClient hostName, tcpConnectTimeout) c <- do sock <- connectTCP port mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e) let tCfg = clientTransportConfig cfg - tcpConnectTimeout `timeout` connectTLS (Just hostName) tCfg clientParams sock >>= \case + tlsTimeout `timeout` connectTLS (Just hostName) tCfg clientParams sock >>= \case Nothing -> do close sock logError "connection timed out" From 9e49c289b4273e69ed763b38b58bd45adb299065 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 1 May 2024 00:51:08 +0100 Subject: [PATCH 008/125] upgrade SMP/NTF servers to v7/v2 protocol versions (#996) * upgrade SMP/NTF servers to v7/v2 protocol versions * 5.6.0.0 --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- src/Simplex/Messaging/Notifications/Transport.hs | 2 +- src/Simplex/Messaging/Transport.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index 022403471..e2c287437 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -47,7 +47,7 @@ currentClientNTFVersion :: VersionNTF currentClientNTFVersion = VersionNTF 1 currentServerNTFVersion :: VersionNTF -currentServerNTFVersion = VersionNTF 1 +currentServerNTFVersion = VersionNTF 2 supportedClientNTFVRange :: VersionRangeNTF supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVersion diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 519154bb5..8dfd15813 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -153,7 +153,7 @@ currentClientSMPRelayVersion :: VersionSMP currentClientSMPRelayVersion = VersionSMP 6 currentServerSMPRelayVersion :: VersionSMP -currentServerSMPRelayVersion = VersionSMP 6 +currentServerSMPRelayVersion = VersionSMP 7 -- minimal supported protocol version is 4 -- TODO remove code that supports sending commands without batching From 60403955c05b9f8e72709fc724d5f9f68b216ea4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 1 May 2024 00:56:33 +0100 Subject: [PATCH 009/125] 5.7.0.4 --- CHANGELOG.md | 20 ++++++++++++++++++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e06ac1caa..ad8862b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 5.7.0 + +Version 5.7.0.4 + +_Please note_: the earliest SimpleX Chat clients supported by this version of the servers is 5.5.3 (released on February 11, 2024). + +SMP server: +- increase max SMP protocol version to 7 (support for deniable authenticators). + +NTF server: +- increase max NTF protocol version to 2 (support for deniable authenticators). + +XFTP server: +- version handshake using ALPN. + +SMP agent: +- increase timeouts for XFTP files. +- don't send commands after timeout. +- PQ encryption support. + # 5.6.2 Version 5.6.2.2. diff --git a/package.yaml b/package.yaml index b4c1f1d78..084fe3a8f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.0.3 +version: 5.7.0.4 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index f353925bd..1bdd67c0b 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.0.3 +version: 5.7.0.4 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From c5941b790b10e0896fb9c8c59b5123657f827003 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 2 May 2024 15:14:01 +0100 Subject: [PATCH 010/125] client: increase timeout for SOCKS connection, increase timeout for direct connection (#1123) --- src/Simplex/Messaging/Transport/Client.hs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Messaging/Transport/Client.hs b/src/Simplex/Messaging/Transport/Client.hs index 08cff1d0d..a943b36d9 100644 --- a/src/Simplex/Messaging/Transport/Client.hs +++ b/src/Simplex/Messaging/Transport/Client.hs @@ -19,7 +19,7 @@ module Simplex.Messaging.Transport.Client TransportHost (..), TransportHosts (..), TransportHosts_ (..), - validateCertificateChain + validateCertificateChain, ) where @@ -52,7 +52,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll, parseString) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.KeepAlive -import Simplex.Messaging.Util (bshow, (<$?>), catchAll, tshow) +import Simplex.Messaging.Util (bshow, catchAll, tshow, (<$?>)) import System.IO.Error import System.Timeout (timeout) import Text.Read (readMaybe) @@ -143,14 +143,19 @@ runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, serverCert <- newEmptyTMVarIO let hostName = B.unpack $ strEncode host clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert - connectTCP = case socksProxy of - Just proxy -> connectSocksClient proxy proxyUsername $ hostAddr host - _ -> connectTCPClient hostName + (connectTCP, tlsTimeout) = case socksProxy of + -- We use a much larger timeout for connections via SOCKS proxy, to allow the circuits created + -- in the socket connection that would otherwise timeout to be used in the next connection attempt. + -- Using standard timeout results in permanent timeout for the clients using SOCKS in cases + -- when SOCKS proxy is very slow (bad network, congestion in underlying network, etc.), + -- because SOCKS proxy destroys circuits when the last session using them is closed. + Just proxy -> (connectSocksClient proxy proxyUsername (hostAddr host), tcpConnectTimeout * 10) + _ -> (connectTCPClient hostName, tcpConnectTimeout) c <- do sock <- connectTCP port mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e) let tCfg = clientTransportConfig cfg - tcpConnectTimeout `timeout` connectTLS (Just hostName) tCfg clientParams sock >>= \case + tlsTimeout `timeout` connectTLS (Just hostName) tCfg clientParams sock >>= \case Nothing -> do close sock logError "connection timed out" From 8d8010a62aef2241fec3876fcfe57d51456b2bc0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 2 May 2024 16:22:55 +0100 Subject: [PATCH 011/125] 5.7.1.0 --- CHANGELOG.md | 5 +++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8862b0f..ffdaa7ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 5.7.1 + +SMP agent: +- increase timeout for TLS connection via SOCKS + # 5.7.0 Version 5.7.0.4 diff --git a/package.yaml b/package.yaml index 084fe3a8f..ccf0794ae 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.0.4 +version: 5.7.1.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 1bdd67c0b..9b5fa36b0 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.0.4 +version: 5.7.1.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From b586a6e90af5eef0e974ac9209f7ee20a7bf6d35 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 3 May 2024 22:16:52 +0100 Subject: [PATCH 012/125] client: removed concurrency limit when waiting for subscription results (#1126) --- src/Simplex/Messaging/Client.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index f54fe686f..8d3c5e54f 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -718,7 +718,7 @@ sendBatch c@ProtocolClient {client_ = PClient {rcvConcurrency, sndQ}} b = do | n > 0 -> do active <- newTVarIO True atomically $ writeTBQueue sndQ (active, s) - pooledMapConcurrentlyN rcvConcurrency (getResponse c active) rs + mapConcurrently (getResponse c active) rs | otherwise -> pure [] TBTransmission s r -> do active <- newTVarIO True From 0e205e70adbe269d43914ef62f04bbbfd04fbeec Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Sat, 4 May 2024 01:39:00 +0300 Subject: [PATCH 013/125] add TRcvQueues tests (#1117) Co-authored-by: Evgeny Poberezkin --- tests/CoreTests/TRcvQueuesTests.hs | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/CoreTests/TRcvQueuesTests.hs b/tests/CoreTests/TRcvQueuesTests.hs index 91722228b..2b0009344 100644 --- a/tests/CoreTests/TRcvQueuesTests.hs +++ b/tests/CoreTests/TRcvQueuesTests.hs @@ -22,10 +22,13 @@ tRcvQueuesTests = do describe "connection API" $ do it "hasConn" hasConnTest it "hasConn, batch add" hasConnTestBatch + it "hasConn, batch idempotent" batchIdempotentTest it "deleteConn" deleteConnTest describe "session API" $ do it "getSessQueues" getSessQueuesTest it "getDelSessQueues" getDelSessQueuesTest + describe "queue transfer" $ do + it "getDelSessQueues-batchAddQueues preserves total length" removeSubsTest checkDataInvariant :: RQ.TRcvQueues -> IO Bool checkDataInvariant trq = atomically $ do @@ -62,6 +65,19 @@ hasConnTestBatch = do atomically (RQ.hasConn "c3" trq) `shouldReturn` True atomically (RQ.hasConn "nope" trq) `shouldReturn` False +batchIdempotentTest :: IO () +batchIdempotentTest = do + trq <- atomically RQ.empty + let qs = [dummyRQ 0 "smp://1234-w==@alpha" "c1", dummyRQ 0 "smp://1234-w==@alpha" "c2", dummyRQ 0 "smp://1234-w==@beta" "c3"] + atomically $ RQ.batchAddQueues trq qs + checkDataInvariant trq `shouldReturn` True + qs' <- readTVarIO $ RQ.getRcvQueues trq + cs' <- readTVarIO $ RQ.getConnections trq + atomically $ RQ.batchAddQueues trq qs + checkDataInvariant trq `shouldReturn` True + readTVarIO (RQ.getRcvQueues trq) `shouldReturn` qs' + fmap L.nub <$> readTVarIO (RQ.getConnections trq) `shouldReturn`cs' -- connections get duplicated, but that doesn't appear to affect anybody + deleteConnTest :: IO () deleteConnTest = do trq <- atomically RQ.empty @@ -121,6 +137,40 @@ getDelSessQueuesTest = do atomically (RQ.hasConn "c3" trq) `shouldReturn` True atomically (RQ.hasConn "c4" trq) `shouldReturn` True +removeSubsTest :: IO () +removeSubsTest = do + aq <- atomically RQ.empty + let qs = + [ dummyRQ 0 "smp://1234-w==@alpha" "c1", + dummyRQ 0 "smp://1234-w==@alpha" "c2", + dummyRQ 0 "smp://1234-w==@beta" "c3", + dummyRQ 1 "smp://1234-w==@beta" "c4" + ] + atomically $ RQ.batchAddQueues aq qs + + pq <- atomically RQ.empty + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@alpha", Nothing) aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "non-existent") aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@localhost", Nothing) aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "c3") aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + +totalSize :: RQ.TRcvQueues -> RQ.TRcvQueues -> STM (Int, Int) +totalSize a b = do + qsizeA <- M.size <$> readTVar (RQ.getRcvQueues a) + qsizeB <- M.size <$> readTVar (RQ.getRcvQueues b) + csizeA <- M.size <$> readTVar (RQ.getConnections a) + csizeB <- M.size <$> readTVar (RQ.getConnections b) + pure (qsizeA + qsizeB, csizeA + csizeB) + dummyRQ :: UserId -> SMPServer -> ConnId -> RcvQueue dummyRQ userId server connId = RcvQueue From ee8e4067b02c520a41b82ab972e262b24f58cd69 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 5 May 2024 12:12:19 +0100 Subject: [PATCH 014/125] agent: prepare connection record before joining to prevent race conditions (#1128) * agent: prepare connection record before joining to prevent race conditions * prepare connection for contact address as well * clean up --- src/Simplex/Messaging/Agent.hs | 100 +++++++++++++++++-------- src/Simplex/Messaging/Client.hs | 1 - tests/AgentTests/FunctionalAPITests.hs | 12 ++- 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 38112b030..f93d03e35 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -55,6 +55,7 @@ module Simplex.Messaging.Agent deleteConnectionAsync, deleteConnectionsAsync, createConnection, + prepareConnectionToJoin, joinConnection, allowConnection, acceptContact, @@ -149,7 +150,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Util (removePath) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Lock (withLock', withLock) +import Simplex.Messaging.Agent.Lock (withLock, withLock') import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval @@ -160,7 +161,7 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs) -import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOn, pattern PQEncOff, pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -198,7 +199,7 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = liftIO $ newSMPAgentEnv cfg store >>= runReaderT runAgent where runAgent = do - c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask + c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask t <- runAgentThreads c `forkFinally` const (liftIO $ disconnectAgentClient c) atomically . writeTVar acThread . Just =<< mkWeakThreadId t pure c @@ -239,7 +240,7 @@ createUser c = withAgentEnv c .: createUser' c {-# INLINE createUser #-} -- | Delete user record optionally deleting all user's connections on SMP servers -deleteUser :: AgentClient -> UserId -> Bool -> AE () +deleteUser :: AgentClient -> UserId -> Bool -> AE () deleteUser c = withAgentEnv c .: deleteUser' c {-# INLINE deleteUser #-} @@ -288,9 +289,18 @@ createConnection :: AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe createConnection c userId enableNtfs = withAgentEnv c .:: newConn c userId "" enableNtfs {-# INLINE createConnection #-} --- | Join SMP agent connection (JOIN command) -joinConnection :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AE ConnId -joinConnection c userId enableNtfs = withAgentEnv c .:: joinConn c userId "" enableNtfs +-- | Create SMP agent connection without queue (to be joined with joinConnection passing connection ID). +-- This method is required to prevent race condition when confirmation from peer is received before +-- the caller of joinConnection saves connection ID to the database. +-- Instead of it we could send confirmation asynchronously, but then it would be harder to report +-- "link deleted" (SMP AUTH) interactively, so this approach is simpler overall. +prepareConnectionToJoin :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> PQSupport -> AE ConnId +prepareConnectionToJoin c userId enableNtfs = withAgentEnv c .: newConnToJoin c userId "" enableNtfs + +-- | Join SMP agent connection (JOIN command). +joinConnection :: AgentClient -> UserId -> Maybe ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AE ConnId +joinConnection c userId Nothing enableNtfs = withAgentEnv c .:: joinConn c userId "" False enableNtfs +joinConnection c userId (Just connId) enableNtfs = withAgentEnv c .:: joinConn c userId connId True enableNtfs {-# INLINE joinConnection #-} -- | Allow connection to continue after CONF notification (LET command) @@ -575,7 +585,7 @@ processCommand :: AgentClient -> (EntityId, APartyCmd 'Client) -> AM (EntityId, processCommand c (connId, APC e cmd) = second (APC e) <$> case cmd of NEW enableNtfs (ACM cMode) pqIK subMode -> second (INV . ACR cMode) <$> newConn c userId connId enableNtfs cMode Nothing pqIK subMode - JOIN enableNtfs (ACR _ cReq) pqEnc subMode connInfo -> (,OK) <$> joinConn c userId connId enableNtfs cReq connInfo pqEnc subMode + JOIN enableNtfs (ACR _ cReq) pqEnc subMode connInfo -> (,OK) <$> joinConn c userId connId False enableNtfs cReq connInfo pqEnc subMode LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK) ACPT invId pqEnc ownCInfo -> (,OK) <$> acceptContact' c connId True invId ownCInfo pqEnc SMSubscribe RJCT invId -> rejectContact' c connId invId $> (connId, OK) @@ -708,11 +718,14 @@ switchConnectionAsync' c corrId connId = newConn :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> AM (ConnId, ConnectionRequestUri c) newConn c userId connId enableNtfs cMode clientData pqInitKeys subMode = - getSMPServer c userId >>= newConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode + getSMPServer c userId >>= newConnSrv c userId connId False enableNtfs cMode clientData pqInitKeys subMode -newConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) -newConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv = do - connId' <- newConnNoQueues c userId connId enableNtfs cMode (CR.connPQEncryption pqInitKeys) +newConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) +newConnSrv c userId connId hasNewConn enableNtfs cMode clientData pqInitKeys subMode srv = do + connId' <- + if hasNewConn + then pure connId + else newConnNoQueues c userId connId enableNtfs cMode (CR.connPQEncryption pqInitKeys) newRcvConnSrv c userId connId' enableNtfs cMode clientData pqInitKeys subMode srv newRcvConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) @@ -738,18 +751,36 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv withStore' c $ \db -> createRatchetX3dhKeys db connId pk1 pk2 pKem pure (connId, CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eEncryptVRange) -joinConn :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId -joinConn c userId connId enableNtfs cReq cInfo pqSupport subMode = do +newConnToJoin :: forall c. AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> PQSupport -> AM ConnId +newConnToJoin c userId connId enableNtfs cReq pqSup = case cReq of + CRInvitationUri {} -> + lift (compatibleInvitationUri cReq) >>= \case + Just (_, (Compatible (CR.E2ERatchetParams v _ _ _)), aVersion) -> create aVersion (Just v) + Nothing -> throwError $ AGENT A_VERSION + CRContactUri {} -> + lift (compatibleContactUri cReq) >>= \case + Just (_, aVersion) -> create aVersion Nothing + Nothing -> throwError $ AGENT A_VERSION + where + create :: Compatible VersionSMPA -> Maybe CR.VersionE2E -> AM ConnId + create (Compatible connAgentVersion) e2eV_ = do + g <- asks random + let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion e2eV_ + cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} + withStore c $ \db -> createNewConn db g cData SCMInvitation + +joinConn :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId +joinConn c userId connId hasNewConn enableNtfs cReq cInfo pqSupport subMode = do srv <- case cReq of CRInvitationUri ConnReqUriData {crSmpQueues = q :| _} _ -> getNextServer c userId [qServer q] _ -> getSMPServer c userId - joinConnSrv c userId connId enableNtfs cReq cInfo pqSupport subMode srv + joinConnSrv c userId connId hasNewConn enableNtfs cReq cInfo pqSupport subMode srv -startJoinInvitation :: UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM (Compatible VersionSMPA, ConnData, NewSndQueue, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) +startJoinInvitation :: UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM (ConnData, NewSndQueue, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) startJoinInvitation userId connId enableNtfs cReqUri pqSup = lift (compatibleInvitationUri cReqUri) >>= \case - Just (qInfo, (Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_)), aVersion@(Compatible connAgentVersion)) -> do + Just (qInfo, (Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_)), Compatible connAgentVersion) -> do g <- asks random let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) (pk1, pk2, pKem, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ v kem_ pqSupport) @@ -760,7 +791,7 @@ startJoinInvitation userId connId enableNtfs cReqUri pqSup = rc = CR.initSndRatchet rcVs rcDHRr rcDHRs rcParams q <- lift $ newSndQueue userId "" qInfo let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} - pure (aVersion, cData, q, rc, e2eSndParams) + pure (cData, q, rc, e2eSndParams) Nothing -> throwError $ AGENT A_VERSION connRequestPQSupport :: AgentClient -> PQSupport -> ConnectionRequestUri c -> IO (Maybe (VersionSMPA, PQSupport)) @@ -786,40 +817,43 @@ compatibleContactUri (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = AgentConfig {smpClientVRange, smpAgentVRange} <- asks config pure $ (,) - <$> (qUri `compatibleVersion` smpClientVRange) + <$> (qUri `compatibleVersion` smpClientVRange) <*> (crAgentVRange `compatibleVersion` smpAgentVRange) versionPQSupport_ :: VersionSMPA -> Maybe CR.VersionE2E -> PQSupport versionPQSupport_ agentV e2eV_ = PQSupport $ agentV >= pqdrSMPAgentVersion && maybe True (>= CR.pqRatchetE2EEncryptVersion) e2eV_ {-# INLINE versionPQSupport_ #-} -joinConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM ConnId -joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSup subMode srv = +joinConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM ConnId +joinConnSrv c userId connId hasNewConn enableNtfs inv@CRInvitationUri {} cInfo pqSup subMode srv = withInvLock c (strEncode inv) "joinConnSrv" $ do - (aVersion, cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSup + (cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSup g <- asks random (connId', sq) <- withStore c $ \db -> runExceptT $ do - r@(connId', _) <- ExceptT $ createSndConn db g cData q + r@(connId', _) <- + if hasNewConn + then (connId,) <$> ExceptT (updateNewConnSnd db connId q) + else ExceptT $ createSndConn db g cData q liftIO $ createRatchet db connId' rc pure r let cData' = (cData :: ConnData) {connId = connId'} - tryError (confirmQueue aVersion c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case + tryError (confirmQueue c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case Right _ -> pure connId' Left e -> do -- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md void $ withStore' c $ \db -> deleteConn db Nothing connId' throwError e -joinConnSrv c userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode srv = +joinConnSrv c userId connId hasNewConn enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode srv = lift (compatibleContactUri cReqUri) >>= \case Just (qInfo, vrsn) -> do - (connId', cReq) <- newConnSrv c userId connId enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv + (connId', cReq) <- newConnSrv c userId connId hasNewConn enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv sendInvitation c userId qInfo vrsn cReq cInfo pure connId' Nothing -> throwError $ AGENT A_VERSION joinConnSrvAsync :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM () joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSupport subMode srv = do - (_aVersion, cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSupport + (cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSupport q' <- withStore c $ \db -> runExceptT $ do liftIO $ createRatchet db connId rc ExceptT $ updateNewConnSnd db connId q @@ -861,7 +895,7 @@ acceptContact' c connId enableNtfs invId ownConnInfo pqSupport subMode = withCon withStore c (`getConn` contactConnId) >>= \case SomeConn _ (ContactConnection ConnData {userId} _) -> do withStore' c $ \db -> acceptInvitation db invId ownConnInfo - joinConn c userId connId enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do + joinConn c userId connId False enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err _ -> throwError $ CMD PROHIBITED @@ -1207,7 +1241,7 @@ enqueueMessage c cData sq msgFlags aMessage = {-# INLINE enqueueMessage #-} -- this function is used only for sending messages in batch, it returns the list of successes to enqueue additional deliveries -enqueueMessageB :: forall t. (Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) +enqueueMessageB :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) enqueueMessageB c reqs = do cfg <- asks config reqMids <- withStoreBatch c $ \db -> fmap (bindRight $ storeSentMsg db cfg) reqs @@ -1239,7 +1273,7 @@ enqueueSavedMessage :: AgentClient -> ConnData -> AgentMsgId -> SndQueue -> AM' enqueueSavedMessage c cData msgId sq = enqueueSavedMessageB c $ Identity (cData, [sq], msgId) {-# INLINE enqueueSavedMessage #-} -enqueueSavedMessageB :: (Foldable t) => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () +enqueueSavedMessageB :: Foldable t => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () enqueueSavedMessageB c reqs = do -- saving to the database is in the start to avoid race conditions when delivery is read from queue before it is saved void $ withStoreBatch' c $ \db -> concatMap (storeDeliveries db) reqs @@ -2565,8 +2599,8 @@ confirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do storeConfirmation c cData sq e2eEncryption_ =<< mkAgentConfirmation c cData sq srv connInfo subMode lift $ submitPendingMsg c cData sq -confirmQueue :: Compatible VersionSMPA -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () -confirmQueue (Compatible agentVersion) c cData@ConnData {connId, pqSupport} sq srv connInfo e2eEncryption_ subMode = do +confirmQueue :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () +confirmQueue c cData@ConnData {connId, connAgentVersion, pqSupport} sq srv connInfo e2eEncryption_ subMode = do msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed @@ -2578,7 +2612,7 @@ confirmQueue (Compatible agentVersion) c cData@ConnData {connId, pqSupport} sq s void . liftIO $ updateSndIds db connId let pqEnc = CR.pqSupportToEnc pqSupport (encConnInfo, _) <- agentRatchetEncrypt db cData (smpEncode aMessage) e2eEncConnInfoLength (Just pqEnc) currentE2EVersion - pure . smpEncode $ AgentConfirmation {agentVersion, e2eEncryption_, encConnInfo} + pure . smpEncode $ AgentConfirmation {agentVersion = connAgentVersion, e2eEncryption_, encConnInfo} mkAgentConfirmation :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> SubscriptionMode -> AM AgentMessage mkAgentConfirmation c cData sq srv connInfo subMode = do diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 8d3c5e54f..38c36f9f2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -113,7 +113,6 @@ import Simplex.Messaging.Transport.WebSockets (WS) import Simplex.Messaging.Util (bshow, diffToMicroseconds, raceAny_, threadDelay', whenM) import Simplex.Messaging.Version import System.Timeout (timeout) -import UnliftIO (pooledMapConcurrentlyN) -- | 'SMPClient' is a handle used to send commands to a specific SMP server. -- diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 79742efab..d3e4e6924 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -244,7 +244,7 @@ createConnection :: AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe createConnection c userId enableNtfs cMode clientData = A.createConnection c userId enableNtfs cMode clientData (IKNoPQ PQSupportOn) joinConnection :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> AE ConnId -joinConnection c userId enableNtfs cReq connInfo = A.joinConnection c userId enableNtfs cReq connInfo PQSupportOn +joinConnection c userId enableNtfs cReq connInfo = A.joinConnection c userId Nothing enableNtfs cReq connInfo PQSupportOn sendMessage :: AgentClient -> ConnId -> SMP.MsgFlags -> MsgBody -> AE AgentMsgId sendMessage c connId msgFlags msgBody = do @@ -503,7 +503,7 @@ runAgentClientTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> runAgentClientTest pqSupport alice@AgentClient {} bob baseId = runRight_ $ do (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe + aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" pqSupport SMSubscribe ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport allowConnection alice bobId confId "alice's connInfo" @@ -630,7 +630,9 @@ runAgentClientContactTest :: HasCallStack => PQSupport -> AgentClient -> AgentCl runAgentClientContactTest pqSupport alice bob baseId = runRight_ $ do (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe + aliceId <- A.prepareConnectionToJoin bob 1 True qInfo pqSupport + aliceId' <- A.joinConnection bob 1 (Just aliceId) True qInfo "bob's connInfo" pqSupport SMSubscribe + liftIO $ aliceId' `shouldBe` aliceId ("", _, A.REQ invId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport bobId <- acceptContact alice True invId "alice's connInfo" PQSupportOn SMSubscribe @@ -1399,7 +1401,9 @@ makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn makeConnectionForUsers_ :: PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers_ pqSupport alice aliceUserId bob bobUserId = do (bobId, qInfo) <- A.createConnection alice aliceUserId True SCMInvitation Nothing (CR.IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob bobUserId True qInfo "bob's connInfo" pqSupport SMSubscribe + aliceId <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport + aliceId' <- A.joinConnection bob bobUserId (Just aliceId) True qInfo "bob's connInfo" pqSupport SMSubscribe + liftIO $ aliceId' `shouldBe` aliceId ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport allowConnection alice bobId confId "alice's connInfo" From e13b0df539cd259187277ad1fb790fae8d505574 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 5 May 2024 17:05:51 +0100 Subject: [PATCH 015/125] client: remove TLS handshake timeout (#1129) * client: remove TLS handshake timeout * remove comment --- src/Simplex/Messaging/Transport/Client.hs | 36 ++++++++--------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Simplex/Messaging/Transport/Client.hs b/src/Simplex/Messaging/Transport/Client.hs index a943b36d9..da2c6c253 100644 --- a/src/Simplex/Messaging/Transport/Client.hs +++ b/src/Simplex/Messaging/Transport/Client.hs @@ -54,7 +54,6 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Util (bshow, catchAll, tshow, (<$?>)) import System.IO.Error -import System.Timeout (timeout) import Text.Read (readMaybe) import UnliftIO.Exception (IOException) import qualified UnliftIO.Exception as E @@ -139,35 +138,26 @@ runTransportClient :: Transport c => TransportClientConfig -> Maybe ByteString - runTransportClient = runTLSTransportClient supportedParameters Nothing runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a -runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, clientCredentials, alpn} proxyUsername host port keyHash client = do +runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} proxyUsername host port keyHash client = do serverCert <- newEmptyTMVarIO let hostName = B.unpack $ strEncode host clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert - (connectTCP, tlsTimeout) = case socksProxy of - -- We use a much larger timeout for connections via SOCKS proxy, to allow the circuits created - -- in the socket connection that would otherwise timeout to be used in the next connection attempt. - -- Using standard timeout results in permanent timeout for the clients using SOCKS in cases - -- when SOCKS proxy is very slow (bad network, congestion in underlying network, etc.), - -- because SOCKS proxy destroys circuits when the last session using them is closed. - Just proxy -> (connectSocksClient proxy proxyUsername (hostAddr host), tcpConnectTimeout * 10) - _ -> (connectTCPClient hostName, tcpConnectTimeout) + connectTCP = case socksProxy of + Just proxy -> connectSocksClient proxy proxyUsername (hostAddr host) + _ -> connectTCPClient hostName c <- do sock <- connectTCP port mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e) let tCfg = clientTransportConfig cfg - tlsTimeout `timeout` connectTLS (Just hostName) tCfg clientParams sock >>= \case - Nothing -> do - close sock - logError "connection timed out" - fail "connection timed out" - Just tls -> do - chain <- - atomically (tryTakeTMVar serverCert) >>= \case - Nothing -> do - logError "onServerCertificate didn't fire or failed to get cert chain" - closeTLS tls >> error "onServerCertificate failed" - Just c -> pure c - getClientConnection tCfg chain tls + -- No TLS timeout to avoid failing connections via SOCKS + tls <- connectTLS (Just hostName) tCfg clientParams sock + chain <- + atomically (tryTakeTMVar serverCert) >>= \case + Nothing -> do + logError "onServerCertificate didn't fire or failed to get cert chain" + closeTLS tls >> error "onServerCertificate failed" + Just c -> pure c + getClientConnection tCfg chain tls client c `E.finally` closeConnection c where hostAddr = \case From 93fd424f86086c6f378b50e343f32ec47f8b0c3f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 5 May 2024 17:13:26 +0100 Subject: [PATCH 016/125] 5.7.2.0 --- CHANGELOG.md | 7 +++++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffdaa7ff2..b5792195d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 5.7.2 + +SMP agent: +- fix connections failing when connecting via link due to race condition on slow network. +- remove concurrency limit when waiting for connection subscription. +- remove TLS timeout. + # 5.7.1 SMP agent: diff --git a/package.yaml b/package.yaml index ccf0794ae..7a536f7bf 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.1.0 +version: 5.7.2.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 9b5fa36b0..7539d675a 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.1.0 +version: 5.7.2.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From c85f6a2f0e6c12272f314e8c4cdf0b313deda4ea Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Tue, 7 May 2024 00:00:42 +0300 Subject: [PATCH 017/125] proxy: reporting errors (#1108) * smp-proxy: iron out errors * treat proxy timeouts as temporary * update errors * proxy errors (missing encoding) * update * enable tests * update * update * fix * fix * simplify * test --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Agent.hs | 4 +- src/Simplex/Messaging/Agent/Client.hs | 25 ++++- src/Simplex/Messaging/Agent/Protocol.hs | 28 ++--- src/Simplex/Messaging/Client.hs | 121 +++++++++++++++------- src/Simplex/Messaging/Protocol.hs | 129 ++++++++++++++++-------- src/Simplex/Messaging/Server.hs | 59 +++++------ tests/CoreTests/ProtocolErrorTests.hs | 45 ++++++--- tests/SMPProxyTests.hs | 8 +- 8 files changed, 266 insertions(+), 153 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3b68667e4..3a0e13157 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -2250,9 +2250,9 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, | otherwise -> ignored _ -> ignored ignored = pure "END from disconnected client - ignored" - _ -> do + r -> do logServer "<--" c srv rId $ "unexpected: " <> bshow cmd - notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED + notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED $ take 32 $ show r where notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m () notify = atomically . notify' diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index c29e35499..35015af5a 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1032,8 +1032,15 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) sendViaProxy destSess = withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do - liftClient SMP (clientServer smp) $ proxySMPMessage smp proxySess spKey_ senderId msgFlags msg - pure . Just $ protocolClientServer' smp + liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case + Right () -> pure . Just $ protocolClientServer' smp + Left proxyErr -> + throwError + PROXY + { proxyServer = protocolClientServer smp, + relayServer = B.unpack $ strEncode destSrv, + proxyErr + } sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg @@ -1066,7 +1073,7 @@ protocolClientError :: (Show err, Encoding err) => (err -> AgentErrorType) -> Ho protocolClientError protocolError_ host = \case PCEProtocolError e -> protocolError_ e PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e - PCEUnexpectedResponse _ -> BROKER host UNEXPECTED + PCEUnexpectedResponse r -> BROKER host $ UNEXPECTED $ take 32 $ show r PCEResponseTimeout -> BROKER host TIMEOUT PCENetworkError -> BROKER host NETWORK PCEIncompatibleHost -> BROKER host HOST @@ -1263,15 +1270,23 @@ processSubResult c rq r = do temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case - BROKER _ NETWORK -> True - BROKER _ TIMEOUT -> True + BROKER _ e -> tempBrokerError e + SMP (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e INACTIVE -> True _ -> False + where + tempBrokerError = \case + NETWORK -> True + TIMEOUT -> True + _ -> False {-# INLINE temporaryAgentError #-} temporaryOrHostError :: AgentErrorType -> Bool temporaryOrHostError = \case BROKER _ HOST -> True + SMP (SMP.PROXY (SMP.BROKER HOST)) -> True + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True e -> temporaryAgentError e {-# INLINE temporaryOrHostError #-} diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 895a8ddc1..61ab2400b 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -189,6 +189,7 @@ import Simplex.FileTransfer.Description import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Transport (XFTPErrorType) import Simplex.Messaging.Agent.QueryString +import Simplex.Messaging.Client (ProxyClientError) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), @@ -206,6 +207,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol ( AProtocolType, + BrokerErrorType (..), EntityId, ErrorType, MsgBody, @@ -233,7 +235,7 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP) +import Simplex.Messaging.Transport (Transport (..), serializeTransportError, transportErrorP) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -1474,6 +1476,8 @@ data AgentErrorType NTF {ntfErr :: ErrorType} | -- | XFTP protocol errors forwarded to agent clients XFTP {xftpErr :: XFTPErrorType} + | -- | SMP proxy errors + PROXY {proxyServer :: String, relayServer :: String, proxyErr :: ProxyClientError} | -- | XRCP protocol errors forwarded to agent clients RCP {rcpErr :: RCErrorType} | -- | SMP server errors @@ -1516,22 +1520,6 @@ data ConnectionErrorType NOT_AVAILABLE deriving (Eq, Read, Show, Exception) --- | SMP server errors. -data BrokerErrorType - = -- | invalid server response (failed to parse) - RESPONSE {smpErr :: String} - | -- | unexpected response - UNEXPECTED - | -- | network error - NETWORK - | -- | no compatible server host (e.g. onion when public is required, or vice versa) - HOST - | -- | handshake or other transport error - TRANSPORT {transportErr :: TransportError} - | -- | command response timeout - TIMEOUT - deriving (Eq, Read, Show, Exception) - -- | Errors of another SMP agent. data SMPAgentError = -- | client or agent message that failed to parse @@ -1587,8 +1575,10 @@ instance StrEncoding AgentErrorType where <|> "SMP " *> (SMP <$> strP) <|> "NTF " *> (NTF <$> strP) <|> "XFTP " *> (XFTP <$> strP) + <|> "PROXY " *> (PROXY <$> textP <* A.space <*> textP <*> _strP) <|> "RCP " *> (RCP <$> strP) <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) + <|> "BROKER " *> (BROKER <$> textP <* " UNEXPECTED " <*> (UNEXPECTED <$> textP)) <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) <|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1) <|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString) @@ -1605,8 +1595,10 @@ instance StrEncoding AgentErrorType where SMP e -> "SMP " <> strEncode e NTF e -> "NTF " <> strEncode e XFTP e -> "XFTP " <> strEncode e + PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] RCP e -> "RCP " <> strEncode e BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e + BROKER srv (UNEXPECTED e) -> "BROKER " <> text srv <> " UNEXPECTED " <> text e BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e @@ -1977,8 +1969,6 @@ $(J.deriveJSON (sumTypeJSON id) ''CommandErrorType) $(J.deriveJSON (sumTypeJSON id) ''ConnectionErrorType) -$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) - $(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError) $(J.deriveJSON (sumTypeJSON id) ''SMPAgentError) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 2936e3841..8a1349089 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -64,6 +64,7 @@ module Simplex.Messaging.Client -- * Supporting types and client configuration ProtocolClientError (..), SMPClientError, + ProxyClientError (..), ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), @@ -97,6 +98,7 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J +import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -534,16 +536,17 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} +-- converts error of client running on proxy to the error sent to client connected to proxy smpProxyError :: SMPClientError -> ErrorType smpProxyError = \case - PCEProtocolError et -> PROXY (PROTOCOL et) - PCEResponseError et -> PROXY (RESPONSE et) - PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs) - PCEResponseTimeout -> PROXY TIMEOUT - PCENetworkError -> PROXY NETWORK - PCEIncompatibleHost -> PROXY BAD_HOST - PCETransportError t -> PROXY (TRANSPORT t) - PCECryptoError _ -> INTERNAL + PCEProtocolError e -> PROXY $ PROTOCOL e + PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e + PCEUnexpectedResponse s -> PROXY $ BROKER $ UNEXPECTED $ B.unpack $ B.take 32 s + PCEResponseTimeout -> PROXY $ BROKER TIMEOUT + PCENetworkError -> PROXY $ BROKER NETWORK + PCEIncompatibleHost -> PROXY $ BROKER HOST + PCETransportError t -> PROXY $ BROKER $ TRANSPORT t + PCECryptoError _ -> CRYPTO PCEIOError _ -> INTERNAL -- | Create a new SMP queue. @@ -699,21 +702,18 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO deleteSMPQueues = okSMPCommands DEL {-# INLINE deleteSMPQueues #-} --- TODO picture - -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - PKEY sId vr (chain, key) -> do + PKEY sId vr (chain, key) -> case supportedClientSMPRelayVRange `compatibleVersion` vr of - Nothing -> throwE PCEIncompatibleHost -- TODO different error - Just (Compatible v) -> liftEitherWith x509Error $ ProxiedRelay sId v <$> validateRelay chain key + Nothing -> throwE $ relayErr VERSION + Just (Compatible v) -> liftEitherWith (const $ relayErr IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key r -> throwE . PCEUnexpectedResponse $ bshow r where - x509Error :: String -> SMPClientError - x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error + relayErr = PCEProtocolError . PROXY . BROKER . TRANSPORT . TEHandshake validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do serverKey <- case cert of @@ -730,10 +730,53 @@ data ProxiedRelay = ProxiedRelay prServerKey :: C.PublicKeyX25519 } +data ProxyClientError + = -- | protocol error response from proxy + ProxyProtocolError ErrorType + | -- | unexpexted response + ProxyUnexpectedResponse String + | -- | error between proxy and server + ProxyResponseError ErrorType + deriving (Eq, Show, Exception) + +instance StrEncoding ProxyClientError where + strEncode = \case + ProxyProtocolError e -> "PROTOCOL " <> strEncode e + ProxyUnexpectedResponse s -> "UNEXPECTED " <> B.pack s + ProxyResponseError e -> "SYNTAX " <> strEncode e + strP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> ProxyProtocolError <$> _strP + "UNEXPECTED" -> ProxyUnexpectedResponse . B.unpack <$> (A.space *> A.takeByteString) + "SYNTAX" -> ProxyResponseError <$> _strP + _ -> fail "bad ProxyClientError" + -- consider how to process slow responses - is it handled somehow locally or delegated to the caller -- this method is used in the client -- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender -- receives PRES :: EncResponse -> BrokerMsg -- proxy to client + +-- When client sends message via proxy, there may be one successful scenario and 9 error scenarios +-- as shown below (WTF stands for unexpected response, ??? for response that failed to parse). +-- client proxy relay proxy client +-- 0) PFWD(SEND) -> RFWD -> RRES -> PRES(OK) -> ok +-- 1) PFWD(SEND) -> RFWD -> RRES -> PRES(ERR) -> PCEProtocolError - business logic error for client +-- 2) PFWD(SEND) -> RFWD -> RRES -> PRES(WTF) -> PCEUnexpectedReponse - relay/client protocol logic error +-- 3) PFWD(SEND) -> RFWD -> RRES -> PRES(???) -> PCEResponseError - relay/client syntax error +-- 4) PFWD(SEND) -> RFWD -> ERR -> ERR PROXY PROTOCOL -> ProxyProtocolError - proxy/relay business logic error +-- 5) PFWD(SEND) -> RFWD -> WTF -> ERR PROXY $ BROKER (UNEXPECTED s) -> ProxyProtocolError - proxy/relay protocol logic +-- 6) PFWD(SEND) -> RFWD -> ??? -> ERR PROXY $ BROKER (RESPONSE s) -> ProxyProtocolError - - proxy/relay syntax +-- 7) PFWD(SEND) -> ERR -> ProxyProtocolError - client/proxy business logic +-- 8) PFWD(SEND) -> WTF -> ProxyUnexpectedResponse - client/proxy protocol logic +-- 9) PFWD(SEND) -> ??? -> ProxyResponseError - client/proxy syntax +-- +-- We report as proxySMPMessage error (ExceptT error) the errors of two kinds: +-- - protocol errors from the destination relay wrapped in PRES - to simplify processing of AUTH and QUOTA errors, in this case proxy is "transparent" for such errors (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other response/transport/connection errors from the client connected to proxy itself +-- Other errors are reported in the function result as `Either ProxiedRelayError ()`, including +-- - protocol errors from the client connected to proxy in ProxyClientError (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError + proxySMPMessage :: SMPClient -> -- proxy session from PKEY @@ -743,7 +786,7 @@ proxySMPMessage :: SenderId -> MsgFlags -> MsgBody -> - ExceptT SMPClientError IO () + ExceptT SMPClientError IO (Either ProxyClientError ()) -- TODO use version proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do -- prepare params @@ -756,24 +799,32 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of - [] -> throwE $ PCETransportError TELargeMsg -- some other error. Internal? - TBError e _ : _ -> throwE $ PCETransportError e -- large message error? + [] -> throwE $ PCETransportError TELargeMsg + TBError e _ : _ -> throwE $ PCETransportError e TBTransmission s _ : _ -> pure s TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength - sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case - -- TODO support PKEY + resend? - PRES (EncResponse er) -> do - t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er - case tParse proxyThParams t' of - t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of - (_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify - Left e -> throwE $ PCEResponseError e - Right OK -> pure () - Right (ERR e) -> throwE $ PCEProtocolError e - Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy - _ -> throwE $ PCETransportError TEBadBlock - r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy + -- proxy interaction errors are wrapped + tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et))) >>= \case + Right r -> case r of + PRES (EncResponse er) -> do + -- server interaction errors are thrown directly + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + case tParse proxyThParams t' of + t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + (_auth, _signed, (_c, _e, cmd)) -> case cmd of + Right OK -> pure $ Right () + Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay + Right e -> throwE $ PCEUnexpectedResponse $ B.take 32 $ bshow e + Left e -> throwE $ PCEResponseError e + _ -> throwE $ PCETransportError TEBadBlock + ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left + _ -> pure . Left $ ProxyUnexpectedResponse $ take 32 $ show r + Left e -> case e of + PCEProtocolError e' -> pure . Left $ ProxyProtocolError e' + PCEUnexpectedResponse r -> pure . Left $ ProxyUnexpectedResponse $ B.unpack r + PCEResponseError e' -> pure . Left $ ProxyResponseError e' + _ -> throwE e -- this method is used in the proxy -- sends RFWD :: EncFwdTransmission -> Command Sender @@ -783,8 +834,8 @@ forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of - Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key? - Just THAuthClient {sessSecret} -> maybe (throwError $ PCEProtocolError INTERNAL) pure sessSecret + Nothing -> throwError $ PCETransportError TENoServerAuth + Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} @@ -796,7 +847,7 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' pure fwdResponse - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE . PCEUnexpectedResponse $ B.take 32 $ bshow r okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = @@ -933,3 +984,5 @@ $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) $(J.deriveJSON defaultJSON ''NetworkConfig) + +$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 118f3b084..d86d65251 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -66,6 +66,7 @@ module Simplex.Messaging.Protocol ErrorType (..), CommandError (..), ProxyError (..), + BrokerErrorType (..), Transmission, TransmissionAuth (..), SignedTransmission, @@ -176,6 +177,7 @@ module Simplex.Messaging.Protocol where import Control.Applicative (optional, (<|>)) +import Control.Exception (Exception) import Control.Monad import Control.Monad.Except import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -194,8 +196,6 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -211,7 +211,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1144,6 +1144,8 @@ data ErrorType PROXY {proxyErr :: ProxyError} | -- | command authorization error - bad signature or non-existing SMP queue AUTH + | -- | encryption/decryption error in proxy protocol + CRYPTO | -- | SMP queue capacity is exceeded on the server QUOTA | -- | ACK command is sent without message to be acknowledged @@ -1186,19 +1188,32 @@ data CommandError data ProxyError = -- | Correctly parsed SMP server ERR response. - -- This error is forwarded to the agent client as `ERR SMP err`. + -- This error is forwarded to the agent client as AgentErrorType `ERR PROXY PROTOCOL err`. PROTOCOL {protocolErr :: ErrorType} - | -- | Invalid server response that failed to parse. - -- Forwarded to the agent client as `ERR BROKER RESPONSE`. - RESPONSE {responseErr :: ErrorType} - | UNEXPECTED {unexpectedResponse :: String} -- 'String' for using derived JSON and Arbitrary instances - | TIMEOUT - | NETWORK - | BAD_HOST - | NO_SESSION - | TRANSPORT {transportErr :: TransportError} + | -- | destination server error + BROKER {brokerErr :: BrokerErrorType} + | -- | basic auth provided to proxy is invalid + BASIC_AUTH + | -- no destination server error + NO_SESSION deriving (Eq, Read, Show) +-- | SMP server errors. +data BrokerErrorType + = -- | invalid server response (failed to parse) + RESPONSE {respErr :: String} + | -- | unexpected response + UNEXPECTED {respErr :: String} + | -- | network error + NETWORK + | -- | no compatible server host (e.g. onion when public is required, or vice versa) + HOST + | -- | handshake or other transport error + TRANSPORT {transportErr :: TransportError} + | -- | command response timeout + TIMEOUT + deriving (Eq, Read, Show, Exception) + -- | SMP transmission parser. transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do @@ -1423,6 +1438,7 @@ instance Encoding ErrorType where CMD err -> "CMD " <> smpEncode err PROXY err -> "PROXY " <> smpEncode err AUTH -> "AUTH" + CRYPTO -> "CRYPTO" QUOTA -> "QUOTA" EXPIRED -> "EXPIRED" NO_MSG -> "NO_MSG" @@ -1437,13 +1453,14 @@ instance Encoding ErrorType where "CMD" -> CMD <$> _smpP "PROXY" -> PROXY <$> _smpP "AUTH" -> pure AUTH + "CRYPTO" -> pure CRYPTO "QUOTA" -> pure QUOTA "EXPIRED" -> pure EXPIRED "NO_MSG" -> pure NO_MSG "LARGE_MSG" -> pure LARGE_MSG "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ - _ -> fail "bad error type" + _ -> fail "bad ErrorType" instance Encoding CommandError where smpEncode e = case e of @@ -1462,45 +1479,71 @@ instance Encoding CommandError where "HAS_AUTH" -> pure HAS_AUTH "NO_ENTITY" -> pure NO_ENTITY "NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility - _ -> fail "bad command error type" + _ -> fail "bad CommandError" instance Encoding ProxyError where - smpEncode e = case e of - PROTOCOL et -> "PROTOCOL " <> smpEncode et - RESPONSE et -> "RESPONSE " <> smpEncode et - UNEXPECTED s -> "UNEXPECTED " <> smpEncode (encodeUtf8 $ T.pack s) - TIMEOUT -> "TIMEOUT" - NETWORK -> "NETWORK" - BAD_HOST -> "BAD_HOST" + smpEncode = \case + PROTOCOL e -> "PROTOCOL " <> smpEncode e + BROKER e -> "BROKER " <> smpEncode e + BASIC_AUTH -> "BASIC_AUTH" NO_SESSION -> "NO_SESSION" - TRANSPORT t -> "TRANSPORT " <> serializeTransportError t smpP = A.takeTill (== ' ') >>= \case "PROTOCOL" -> PROTOCOL <$> _smpP - "RESPONSE" -> RESPONSE <$> _smpP - "UNEXPECTED" -> UNEXPECTED . (T.unpack . safeDecodeUtf8) <$> _smpP - "TIMEOUT" -> pure TIMEOUT - "NETWORK" -> pure NETWORK - "BAD_HOST" -> pure BAD_HOST + "BROKER" -> BROKER <$> _smpP + "BASIC_AUTH" -> pure BASIC_AUTH "NO_SESSION" -> pure NO_SESSION - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) - _ -> fail "bad command error type" + _ -> fail "bad ProxyError" instance StrEncoding ProxyError where strEncode = \case - PROTOCOL et -> "PROTOCOL " <> strEncode et - RESPONSE et -> "RESPONSE " <> strEncode et - UNEXPECTED "" -> "UNEXPECTED" -- Arbitrary instance generates empty strings which String instance can't handle - UNEXPECTED s -> "UNEXPECTED " <> strEncode s - TRANSPORT t -> "TRANSPORT " <> serializeTransportError t - e -> bshow e + PROTOCOL e -> "PROTOCOL " <> strEncode e + BROKER e -> "BROKER " <> strEncode e + BASIC_AUTH -> "BASIC_AUTH" + NO_SESSION -> "NO_SESSION" strP = - "PROTOCOL " *> (PROTOCOL <$> strP) - <|> "RESPONSE " *> (RESPONSE <$> strP) - <|> "UNEXPECTED " *> (UNEXPECTED <$> strP) - <|> "UNEXPECTED" $> UNEXPECTED "" - <|> "TRANSPORT " *> (TRANSPORT <$> transportErrorP) - <|> parseRead1 + 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 " <> serializeTransportError e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + smpP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _smpP + "UNEXPECTED" -> UNEXPECTED <$> _smpP + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" + +instance StrEncoding BrokerErrorType where + strEncode = \case + RESPONSE e -> "RESPONSE " <> strEncode e + UNEXPECTED e -> "UNEXPECTED " <> strEncode e + TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + strP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _strP + "UNEXPECTED" -> UNEXPECTED <$> _strP + "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" -- | Send signed SMP transmission to TCP transport. tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] @@ -1639,5 +1682,7 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) +$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) + -- run deriveJSON in one TH splice to allow mutual instance $(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType]) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index d1fcfbc24..0c0426958 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -54,7 +54,7 @@ import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import Data.List (intercalate) -import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (isNothing) @@ -68,7 +68,7 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), forwardSMPMessage, smpProxyError) +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError) import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -616,28 +616,27 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi reply = atomically . writeTBQueue sndQ processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of - PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR AUTH) + PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR $ PROXY BASIC_AUTH) where allowProxy = do ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth getRelay = do ProxyAgent {smpAgent} <- asks proxyAgent - -- TODO catch IO errors too - liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) + liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) `catch` (pure . Left . PCEIOError) where proxyResp = \case + Left err -> ERR $ smpProxyError err Right smp -> let THandleParams {sessionId = srvSessId, thAuth} = thParams smp vr = supportedServerSMPRelayVRange in case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR $ PROXY (TRANSPORT TENoServerAuth) - Left err -> ERR $ smpProxyError err + Nothing -> ERR . PROXY . BROKER $ TRANSPORT TENoServerAuth PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case - Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) + Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) Nothing -> pure $ ERR $ PROXY NO_SESSION processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do @@ -916,50 +915,48 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi pure . (cbNonce,) $ fromRight "" encNMsgMeta processForwardedCommand :: EncFwdTransmission -> M BrokerMsg - processForwardedCommand (EncFwdTransmission s) = fmap (either id id) . runExceptT $ do - -- TODO error - THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwError $ ERR INTERNAL) pure thAuth - sessSecret <- maybe (throwError $ ERR INTERNAL) pure sessSecret' + processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do + THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE noRelayAuth) pure (thAuth thParams') + sessSecret <- maybe (throwE noRelayAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId - -- TODO error - s' <- liftEitherWith internalErr $ C.cbDecryptNoPad sessSecret proxyNonce s - -- TODO error - FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s' - -- TODO error - this error is reported to proxy, as we failed to get to client's transmission + s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s + FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' let clientSecret = C.dh' fwdKey serverPrivKey clientNonce = C.cbNonce $ bs fwdCorrId - b <- liftEitherWith internalErr $ C.cbDecrypt clientSecret clientNonce et + b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et -- only allowing single forwarded transactions - let t' = tDecodeParseValidate thParams' $ L.head $ tParse thParams' b - clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} - -- TODO error + t' <- case tParse thParams' b of + t :| [] -> pure $ tDecodeParseValidate thParams' t + _ -> throwE BLOCK + let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} + -- process forwarded SEND r <- lift (rejectOrVerify clntThAuth t') >>= \case Left r -> pure r - Right t''@(_, (corrId', entId', _)) -> - -- Left will not be returned by processCommand, as only SEND command is allowed - fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') - + Right t''@(_, (corrId', entId', cmd')) -> case cmd' of + Cmd SSender SEND {} -> + -- Left will not be returned by processCommand, as only SEND command is allowed + fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + _ -> + pure (corrId', entId', ERR $ CMD PROHIBITED) -- encode response r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of - [] -> throwE $ ERR INTERNAL -- TODO error - TBError _ _ : _ -> throwE $ ERR INTERNAL -- TODO error + [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right + TBError _ _ : _ -> throwE BLOCK TBTransmission b' _ : _ -> pure b' TBTransmissions b' _ _ : _ -> pure b' -- encrypt to client - r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where - internalErr _ = ERR INTERNAL -- TODO errors - THandleParams {thAuth} = thParams' + noRelayAuth = PROXY $ BROKER $ TRANSPORT TENoServerAuth rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = case cmdOrError of Left e -> pure $ Left (corrId', entId', ERR e) - -- flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' where verified = \case diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 8f5ad70e7..0bf60afdd 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -6,17 +6,15 @@ module CoreTests.ProtocolErrorTests where -import qualified Data.ByteString.Char8 as B -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.FileTransfer.Transport (XFTPErrorType (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as Agent +import Simplex.Messaging.Client (ProxyClientError (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..)) +import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) @@ -27,21 +25,32 @@ import Test.QuickCheck protocolErrorTests :: Spec protocolErrorTests = modifyMaxSuccess (const 1000) $ do describe "errors parsing / serializing" $ do - it "should parse SMP protocol errors" . property $ \(err :: ErrorType) -> + it "should parse SMP protocol errors" . property . forAll possibleErrorType $ \err -> smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property . forAll possible $ \err -> + it "should parse SMP agent errors" . property . forAll possibleAgentErrorType $ \err -> strDecode (strEncode err) == Right err where - possible :: Gen AgentErrorType - possible = + possibleErrorType :: Gen ErrorType + possibleErrorType = arbitrary >>= \e -> if skipErrorType e then discard else pure e + possibleAgentErrorType :: Gen AgentErrorType + possibleAgentErrorType = arbitrary >>= \case - BROKER srv (Agent.RESPONSE e) | hasSpaces srv || hasSpaces e -> discard - BROKER srv _ | hasSpaces srv -> discard - SMP (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard - NTF (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard + BROKER srv _ | skip srv -> discard + BROKER _ (RESPONSE e) | skip e -> discard + BROKER _ (UNEXPECTED e) | skip e -> discard + SMP e | skipErrorType e -> discard + NTF e | skipErrorType e -> discard + Agent.PROXY pxy srv _ | skip pxy || skip srv -> discard + Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard + Agent.PROXY _ _ (ProxyUnexpectedResponse e) | skip e -> discard + Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard ok -> pure ok - hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) - hasUnicode = any (>= '\255') + skip s = null s || any (\c -> c <= ' ' || c >= '\255') s + skipErrorType = \case + SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e + SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> skip s + SMP.PROXY (SMP.BROKER (RESPONSE s)) -> skip s + _ -> False deriving instance Generic AgentErrorType @@ -49,6 +58,8 @@ deriving instance Generic CommandErrorType deriving instance Generic ConnectionErrorType +deriving instance Generic ProxyClientError + deriving instance Generic BrokerErrorType deriving instance Generic SMPAgentError @@ -59,7 +70,7 @@ deriving instance Generic ErrorType deriving instance Generic CommandError -deriving instance Generic ProxyError +deriving instance Generic SMP.ProxyError deriving instance Generic TransportError @@ -75,6 +86,8 @@ instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU +instance Arbitrary ProxyClientError where arbitrary = genericArbitraryU + instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU @@ -85,7 +98,7 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU instance Arbitrary CommandError where arbitrary = genericArbitraryU -instance Arbitrary ProxyError where arbitrary = genericArbitraryU +instance Arbitrary SMP.ProxyError where arbitrary = genericArbitraryU instance Arbitrary TransportError where arbitrary = genericArbitraryU diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index b70c88883..52145a992 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -113,7 +113,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do -- get proxy session sess <- connectSMPProxiedRelay pc relayServ (Just "correct") -- send via proxy to unsecured queue - proxySMPMessage pc sess Nothing sndId noMsgFlags msg + Right () <- proxySMPMessage pc sess Nothing sndId noMsgFlags msg -- receive 1 (_tSess, _v, _sid, _isResp, _entId, SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId encBody `shouldBe` Right msg @@ -122,7 +122,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g secureSMPQueue rc rPriv rcvId sPub -- send via proxy to secured queue - proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' + Right () <- proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' -- receive 2 (_tSess, _v, _sid, _isResp, _entId, SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ liftIO $ dec msgId' encBody' `shouldBe` Right msg' @@ -171,14 +171,14 @@ testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) - reply `shouldBe` Right (SMP.ERR SMP.AUTH) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") - reply `shouldBe` Right (SMP.ERR SMP.AUTH) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} From a70f492f4dbb9ec997b39ab3ea89c2858b5dde73 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 7 May 2024 13:37:40 +0100 Subject: [PATCH 018/125] proxy: fallback to direct connection if destination relay does not support proxy protocol (#1132) * proxy: fallback to direct connection if destination relay does not support proxy protocol * move version to TransportError, refactor --- src/Simplex/FileTransfer/Client.hs | 4 +- src/Simplex/FileTransfer/Transport.hs | 5 +- src/Simplex/Messaging/Agent/Client.hs | 61 +++++++++----- src/Simplex/Messaging/Agent/Protocol.hs | 83 +++++++++++-------- src/Simplex/Messaging/Client.hs | 30 +++++-- .../Messaging/Notifications/Transport.hs | 5 +- src/Simplex/Messaging/Protocol.hs | 22 +++-- src/Simplex/Messaging/Server.hs | 27 ++++-- src/Simplex/Messaging/Transport.hs | 46 +++++----- tests/AgentTests/FunctionalAPITests.hs | 6 +- tests/CoreTests/ProtocolErrorTests.hs | 17 ++-- tests/SMPAgentClient.hs | 7 +- tests/SMPClient.hs | 5 +- tests/SMPProxyTests.hs | 20 ++++- tests/Test.hs | 2 +- 15 files changed, 212 insertions(+), 128 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index a788e39c2..4efff9388 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -51,7 +51,7 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client @@ -114,7 +114,7 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, thParams@THandleParams {thVersion} <- case sessionALPN of Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 Nothing -> pure thParams0 - _ -> throwError $ PCETransportError (TEHandshake VERSION) + _ -> throwError $ PCETransportError TEVersion logDebug $ "Client negotiated protocol: " <> tshow thVersion let c = XFTPClient {http2Client, thParams, transportSession, config} atomically $ writeTVar clientVar $ Just c diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 27f1b8b95..244e00972 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -37,6 +37,7 @@ import qualified Control.Exception as E import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.Trans.Except import qualified Data.Aeson.TH as J import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first) @@ -53,7 +54,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol (CommandError) -import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) +import Simplex.Messaging.Transport (SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.File import Simplex.Messaging.Util (bshow) import Simplex.Messaging.Version @@ -95,7 +96,7 @@ supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion -- XFTP protocol does not use this handshake method xftpClientHandshakeStub :: c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient) -xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION +xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwE TEVersion data XFTPServerHandshake = XFTPServerHandshake { xftpVersionRange :: VersionRangeXFTP, diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 35015af5a..ab9f3eb5f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -147,6 +147,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import qualified Data.Aeson.TH as J @@ -231,7 +232,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SMPVersion) +import Simplex.Messaging.Transport (SMPVersion, TransportError (..)) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -577,7 +578,7 @@ getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do prs <- atomically TM.empty smpConnectClient c tSess prs v -getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, ProxiedRelay) +getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do unlessM (readTVarIO active) . throwError $ INACTIVE proxySrv <- getNextServer c userId [destSrv] @@ -589,7 +590,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure let tSess = (userId, srv, qId) (tSess,auth,) <$> getSessVar workerSeq tSess smpClients - newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) newProxyClient tSess auth v = do (prs, rv) <- atomically $ do prs <- TM.empty @@ -598,32 +599,33 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq (prs,) . either id id <$> getSessVar workerSeq destSrv prs clnt <- smpConnectClient c tSess prs v (clnt,) <$> newProxiedRelay clnt auth rv - waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, ProxiedRelay) + waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) waitForProxyClient tSess auth v = do clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess v sess <- atomically (getSessVar workerSeq destSrv prs) >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) pure (clnt, sess) - newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM ProxiedRelay + newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) newProxiedRelay clnt@(SMPConnectedClient smp prs) proxyAuth rv = tryAgentError (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp destSrv proxyAuth) >>= \case Right sess -> do atomically $ putTMVar (sessionVar rv) (Right sess) liftIO $ incClientStat c userId clnt "PROXY" "OK" - pure sess + pure $ Right sess Left e -> do liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e atomically $ do - removeSessVar rv destSrv prs - TM.delete destSess smpProxiedRelays + unless (persistentProxyError e) $ do + removeSessVar rv destSrv prs + TM.delete destSess smpProxiedRelays putTMVar (sessionVar rv) (Left e) - throwError e -- signal error to caller - waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM ProxiedRelay + pure $ Left e + waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) waitForProxiedRelay (_, srv, _) rv = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c sess_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar rv) - liftEither $ case sess_ of + pure $ case sess_ of Just (Right sess) -> Right sess Just (Left e) -> Left e Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT @@ -874,6 +876,7 @@ closeAgentClient c = do closeProtocolServerClients c smpClients closeProtocolServerClients c ntfClients closeProtocolServerClients c xftpClients + atomically $ writeTVar (smpProxiedRelays c) M.empty atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect clearWorkers smpDeliveryWorkers >>= mapM_ (cancelWorker . fst) clearWorkers asyncCmdWorkers >>= mapM_ cancelWorker @@ -981,11 +984,14 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do - cp@(cl, _) <- getSMPProxyClient c destSess + (cl, sess_) <- getSMPProxyClient c destSess logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr - r <- (action cp <* stat cl "OK") `catchAgentError` logServerError cl - logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" - pure r + case sess_ of + Right sess -> do + r <- (action (cl, sess) <* stat cl "OK") `catchAgentError` logServerError cl + logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" + pure r + Left e -> logServerError cl e where stat cl = liftIO . incClientStat c userId cl cmdStr proxySrv = showServer . protocolClientServer' . protocolClient @@ -1029,18 +1035,29 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do | ipAddressProtected cfg destSrv -> pure False | otherwise -> unknownServer SPMNever -> pure False + directAllowed = do + cfg <- getNetworkConfig c + pure $ case smpProxyFallback cfg of + SPFAllow -> True + SPFAllowProtected -> ipAddressProtected cfg destSrv + SPFProhibit -> False unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) - sendViaProxy destSess = - withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do + sendViaProxy destSess = do + r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case Right () -> pure . Just $ protocolClientServer' smp Left proxyErr -> - throwError + throwE PROXY { proxyServer = protocolClientServer smp, relayServer = B.unpack $ strEncode destSrv, proxyErr } + case r of + Right r' -> pure r' + Left e + | persistentProxyError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) + | otherwise -> throwE e sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg @@ -1280,7 +1297,6 @@ temporaryAgentError = \case NETWORK -> True TIMEOUT -> True _ -> False -{-# INLINE temporaryAgentError #-} temporaryOrHostError :: AgentErrorType -> Bool temporaryOrHostError = \case @@ -1288,7 +1304,12 @@ temporaryOrHostError = \case SMP (SMP.PROXY (SMP.BROKER HOST)) -> True PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True e -> temporaryAgentError e -{-# INLINE temporaryOrHostError #-} + +persistentProxyError :: AgentErrorType -> Bool +persistentProxyError = \case + BROKER _ (SMP.TRANSPORT TEVersion) -> True + SMP (SMP.PROXY (SMP.BROKER (SMP.TRANSPORT TEVersion))) -> True + _ -> False -- | Subscribe to queues. The list of results can have a different order. subscribeQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 61ab2400b..5cfd7af03 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -235,7 +235,7 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..), serializeTransportError, transportErrorP) +import Simplex.Messaging.Transport (Transport (..)) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -1554,12 +1554,14 @@ data AgentCryptoError instance StrEncoding AgentCryptoError where strP = - "DECRYPT_AES" $> DECRYPT_AES - <|> "DECRYPT_CB" $> DECRYPT_CB - <|> "RATCHET_HEADER" $> RATCHET_HEADER - <|> "RATCHET_EARLIER " *> (RATCHET_EARLIER <$> strP) - <|> "RATCHET_SKIPPED " *> (RATCHET_SKIPPED <$> strP) - <|> "RATCHET_SYNC" $> RATCHET_SYNC + A.takeTill (== ' ') >>= \case + "DECRYPT_AES" -> pure DECRYPT_AES + "DECRYPT_CB" -> pure DECRYPT_CB + "RATCHET_HEADER" -> pure RATCHET_HEADER + "RATCHET_EARLIER" -> RATCHET_EARLIER <$> _strP + "RATCHET_SKIPPED" -> RATCHET_SKIPPED <$> _strP + "RATCHET_SYNC" -> pure RATCHET_SYNC + _ -> fail "AgentCryptoError" strEncode = \case DECRYPT_AES -> "DECRYPT_AES" DECRYPT_CB -> "DECRYPT_CB" @@ -1570,25 +1572,24 @@ instance StrEncoding AgentCryptoError where instance StrEncoding AgentErrorType where strP = - "CMD " *> (CMD <$> parseRead1) - <|> "CONN " *> (CONN <$> parseRead1) - <|> "SMP " *> (SMP <$> strP) - <|> "NTF " *> (NTF <$> strP) - <|> "XFTP " *> (XFTP <$> strP) - <|> "PROXY " *> (PROXY <$> textP <* A.space <*> textP <*> _strP) - <|> "RCP " *> (RCP <$> strP) - <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) - <|> "BROKER " *> (BROKER <$> textP <* " UNEXPECTED " <*> (UNEXPECTED <$> textP)) - <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) - <|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1) - <|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString) - <|> "AGENT QUEUE " *> (AGENT . A_QUEUE <$> parseRead A.takeByteString) - <|> "AGENT " *> (AGENT <$> parseRead1) - <|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString) - <|> "CRITICAL " *> (CRITICAL <$> parseRead1 <* A.space <*> parseRead A.takeByteString) - <|> "INACTIVE" $> INACTIVE + A.takeTill (== ' ') + >>= \case + "CMD" -> CMD <$> (A.space *> parseRead1) + "CONN" -> CONN <$> (A.space *> parseRead1) + "SMP" -> SMP <$> _strP + "NTF" -> NTF <$> _strP + "XFTP" -> XFTP <$> _strP + "PROXY" -> PROXY <$> (A.space *> srvP) <* A.space <*> srvP <*> _strP + "RCP" -> RCP <$> _strP + "BROKER" -> BROKER <$> (A.space *> srvP) <*> _strP + "AGENT" -> AGENT <$> _strP + "INTERNAL" -> INTERNAL <$> (A.space *> textP) + "CRITICAL" -> CRITICAL <$> (A.space *> parseRead1) <*> (A.space *> textP) + "INACTIVE" -> pure INACTIVE + _ -> fail "bad AgentErrorType" where - textP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') + srvP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') + textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString strEncode = \case CMD e -> "CMD " <> bshow e CONN e -> "CONN " <> bshow e @@ -1597,19 +1598,33 @@ instance StrEncoding AgentErrorType where XFTP e -> "XFTP " <> strEncode e PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] RCP e -> "RCP " <> strEncode e - BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e - BROKER srv (UNEXPECTED e) -> "BROKER " <> text srv <> " UNEXPECTED " <> text e - BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e - BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e - AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e - AGENT (A_QUEUE e) -> "AGENT QUEUE " <> bshow e - AGENT e -> "AGENT " <> bshow e - INTERNAL e -> "INTERNAL " <> bshow e - CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> bshow e + BROKER srv e -> B.unwords ["BROKER", text srv, strEncode e] + AGENT e -> "AGENT " <> strEncode e + INTERNAL e -> "INTERNAL " <> encodeUtf8 (T.pack e) + CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> encodeUtf8 (T.pack e) INACTIVE -> "INACTIVE" where text = encodeUtf8 . T.pack +instance StrEncoding SMPAgentError where + strP = + A.takeTill (== ' ') + >>= \case + "MESSAGE" -> pure A_MESSAGE + "PROHIBITED" -> pure A_PROHIBITED + "VERSION" -> pure A_VERSION + "CRYPTO" -> A_CRYPTO <$> _strP + "DUPLICATE" -> pure A_DUPLICATE + "QUEUE" -> A_QUEUE . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + _ -> fail "bad SMPAgentError" + strEncode = \case + A_MESSAGE -> "MESSAGE" + A_PROHIBITED -> "PROHIBITED" + A_VERSION -> "VERSION" + A_CRYPTO e -> "CRYPTO " <> strEncode e + A_DUPLICATE -> "DUPLICATE" + A_QUEUE e -> "QUEUE " <> encodeUtf8 (T.pack e) + cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState cryptoErrToSyncState = \case DECRYPT_AES -> RSAllowed diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 8a1349089..c38e56810 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -70,6 +70,7 @@ module Simplex.Messaging.Client TransportSessionMode (..), HostMode (..), SMPProxyMode (..), + SMPProxyFallback (..), defaultClientConfig, defaultSMPClientConfig, defaultNetworkConfig, @@ -224,6 +225,8 @@ data NetworkConfig = NetworkConfig sessionMode :: TransportSessionMode, -- | SMP proxy mode smpProxyMode :: SMPProxyMode, + -- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions + smpProxyFallback :: SMPProxyFallback, -- | timeout for the initial client TCP/TLS connection (microseconds) tcpConnectTimeout :: Int, -- | timeout of protocol commands (microseconds) @@ -253,6 +256,12 @@ data SMPProxyMode | SPMNever deriving (Eq, Show) +data SMPProxyFallback + = SPFAllow -- connect directly when chosen proxy or destination relay do not support proxy protocol. + | SPFAllowProtected -- connect directly only when IP address is protected (SOCKS proxy or .onion address is used). + | SPFProhibit -- prohibit direct connection to destination relay. + deriving (Eq, Show) + defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig @@ -261,6 +270,7 @@ defaultNetworkConfig = requiredHostMode = False, sessionMode = TSMUser, smpProxyMode = SPMNever, + smpProxyFallback = SPFAllow, tcpConnectTimeout = defaultTcpConnectTimeout, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 5_000, @@ -705,15 +715,17 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay -connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth = - sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case - PKEY sId vr (chain, key) -> - case supportedClientSMPRelayVRange `compatibleVersion` vr of - Nothing -> throwE $ relayErr VERSION - Just (Compatible v) -> liftEitherWith (const $ relayErr IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key - r -> throwE . PCEUnexpectedResponse $ bshow r +connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth + | thVersion (thParams c) >= sendingProxySMPVersion = + sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case + PKEY sId vr (chain, key) -> + case supportedClientSMPRelayVRange `compatibleVersion` vr of + Nothing -> throwE $ transportErr TEVersion + Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key + r -> throwE . PCEUnexpectedResponse $ bshow r + | otherwise = throwE $ PCETransportError TEVersion where - relayErr = PCEProtocolError . PROXY . BROKER . TRANSPORT . TEHandshake + transportErr = PCEProtocolError . PROXY . BROKER . TRANSPORT validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do serverKey <- case cert of @@ -983,6 +995,8 @@ $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback) + $(J.deriveJSON defaultJSON ''NetworkConfig) $(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index e2c287437..564b0c0df 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -11,6 +11,7 @@ module Simplex.Messaging.Notifications.Transport where import Control.Monad (forM) import Control.Monad.Except +import Control.Monad.Trans.Except import Data.Attoparsec.ByteString.Char8 (Parser) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -111,7 +112,7 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do throwError $ TEHandshake IDENTITY | v `isCompatible` ntfVRange -> pure $ ntfThHandleServer th v pk - | otherwise -> throwError $ TEHandshake VERSION + | otherwise -> throwE TEVersion -- | Notifcations server client transport handshake. ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) @@ -128,7 +129,7 @@ ntfClientHandshake c keyHash ntfVRange = do (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash} pure $ ntfThHandleClient th v ck_ - Nothing -> throwError $ TEHandshake VERSION + Nothing -> throwE TEVersion ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer ntfThHandleServer th v pk = diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index d86d65251..250c76fcf 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -196,6 +196,8 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock.System (SystemTime (..)) import Data.Type.Equality import Data.Word (Word16) @@ -211,7 +213,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -1513,7 +1515,7 @@ instance Encoding BrokerErrorType where smpEncode = \case RESPONSE e -> "RESPONSE " <> smpEncode e UNEXPECTED e -> "UNEXPECTED " <> smpEncode e - TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + TRANSPORT e -> "TRANSPORT " <> smpEncode e NETWORK -> "NETWORK" TIMEOUT -> "TIMEOUT" HOST -> "HOST" @@ -1521,7 +1523,7 @@ instance Encoding BrokerErrorType where A.takeTill (== ' ') >>= \case "RESPONSE" -> RESPONSE <$> _smpP "UNEXPECTED" -> UNEXPECTED <$> _smpP - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "TRANSPORT" -> TRANSPORT <$> _smpP "NETWORK" -> pure NETWORK "TIMEOUT" -> pure TIMEOUT "HOST" -> pure HOST @@ -1529,21 +1531,23 @@ instance Encoding BrokerErrorType where instance StrEncoding BrokerErrorType where strEncode = \case - RESPONSE e -> "RESPONSE " <> strEncode e - UNEXPECTED e -> "UNEXPECTED " <> strEncode e - TRANSPORT e -> "TRANSPORT " <> serializeTransportError e + RESPONSE e -> "RESPONSE " <> encodeUtf8 (T.pack e) + UNEXPECTED e -> "UNEXPECTED " <> encodeUtf8 (T.pack e) + TRANSPORT e -> "TRANSPORT " <> smpEncode e NETWORK -> "NETWORK" TIMEOUT -> "TIMEOUT" HOST -> "HOST" strP = A.takeTill (== ' ') >>= \case - "RESPONSE" -> RESPONSE <$> _strP - "UNEXPECTED" -> UNEXPECTED <$> _strP - "TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP) + "RESPONSE" -> RESPONSE <$> _textP + "UNEXPECTED" -> UNEXPECTED <$> _textP + "TRANSPORT" -> TRANSPORT <$> _smpP "NETWORK" -> pure NETWORK "TIMEOUT" -> pure TIMEOUT "HOST" -> pure HOST _ -> fail "bad BrokerErrorType" + where + _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 ()] diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 0c0426958..4195bffba 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -628,16 +628,26 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi proxyResp = \case Left err -> ERR $ smpProxyError err Right smp -> - let THandleParams {sessionId = srvSessId, thAuth} = thParams smp - vr = supportedServerSMPRelayVRange - in case thAuth of - Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR . PROXY . BROKER $ TRANSPORT TENoServerAuth + let THandleParams {sessionId = srvSessId, thVersion, thAuth} = thParams smp + vr = supportedServerSMPRelayVRange -- TODO this should be destination relay version range + in if thVersion >= sendingProxySMPVersion + then case thAuth of + Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey + Nothing -> ERR $ transportErr TENoServerAuth + else ERR $ transportErr TEVersion PFWD pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case - Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) + Just smp + | v >= sendingProxySMPVersion -> + liftIO $ either (ERR . smpProxyError) PRES <$> + runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) + | otherwise -> pure . ERR $ transportErr TEVersion + where + THandleParams {thVersion = v} = thParams smp Nothing -> pure $ ERR $ PROXY NO_SESSION + transportErr :: TransportError -> ErrorType + transportErr = PROXY . BROKER . TRANSPORT processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do st <- asks queueStore @@ -916,8 +926,8 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi processForwardedCommand :: EncFwdTransmission -> M BrokerMsg processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do - THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE noRelayAuth) pure (thAuth thParams') - sessSecret <- maybe (throwE noRelayAuth) pure sessSecret' + THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE $ transportErr TENoServerAuth) pure (thAuth thParams') + sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' @@ -952,7 +962,6 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) pure $ RRES r3 where - noRelayAuth = PROXY $ BROKER $ TRANSPORT TENoServerAuth rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = case cmdOrError of diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6f0f04ff7..6d1f05852 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -72,14 +72,12 @@ module Simplex.Messaging.Transport smpClientHandshake, tPutBlock, tGetBlock, - serializeTransportError, - transportErrorP, sendHandshake, getHandshake, ) where -import Control.Applicative (optional, (<|>)) +import Control.Applicative (optional) import Control.Monad (forM) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) @@ -410,6 +408,8 @@ authEncryptCmdsP v p = if v >= authCmdsSMPVersion then optional p else pure Noth data TransportError = -- | error parsing transport block TEBadBlock + | -- | incompatible client or server version + TEVersion | -- | message does not fit in transport block TELargeMsg | -- | incorrect session ID @@ -425,31 +425,29 @@ data TransportError data HandshakeError = -- | parsing error PARSE - | -- | incompatible peer version - VERSION | -- | incorrect server identity IDENTITY | -- | v7 authentication failed BAD_AUTH deriving (Eq, Read, Show, Exception) --- | SMP encrypted transport error parser. -transportErrorP :: Parser TransportError -transportErrorP = - "BLOCK" $> TEBadBlock - <|> "LARGE_MSG" $> TELargeMsg - <|> "SESSION" $> TEBadSession - <|> "NO_AUTH" $> TENoServerAuth - <|> "HANDSHAKE " *> (TEHandshake <$> parseRead1) - --- | Serialize SMP encrypted transport error. -serializeTransportError :: TransportError -> ByteString -serializeTransportError = \case - TEBadBlock -> "BLOCK" - TELargeMsg -> "LARGE_MSG" - TEBadSession -> "SESSION" - TENoServerAuth -> "NO_AUTH" - TEHandshake e -> "HANDSHAKE " <> bshow e +instance Encoding TransportError where + smpP = + A.takeTill (== ' ') >>= \case + "BLOCK" -> pure TEBadBlock + "VERSION" -> pure TEVersion + "LARGE_MSG" -> pure TELargeMsg + "SESSION" -> pure TEBadSession + "NO_AUTH" -> pure TENoServerAuth + "HANDSHAKE" -> TEHandshake <$> (A.space *> parseRead1) + _ -> fail "bad TransportError" + smpEncode = \case + TEBadBlock -> "BLOCK" + TEVersion -> "VERSION" + TELargeMsg -> "LARGE_MSG" + TEBadSession -> "SESSION" + TENoServerAuth -> "NO_AUTH" + TEHandshake e -> "HANDSHAKE " <> bshow e -- | Pad and send block to SMP transport. tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) @@ -480,7 +478,7 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do throwE $ TEHandshake IDENTITY | v `isCompatible` smpVRange -> pure $ smpThHandleServer th v pk k' - | otherwise -> throwE $ TEHandshake VERSION + | otherwise -> throwE TEVersion -- | Client SMP transport handshake. -- @@ -503,7 +501,7 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} pure $ smpThHandleClient th v (snd <$> ks_) ck_ - Nothing -> throwE $ TEHandshake VERSION + Nothing -> throwE TEVersion smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer smpThHandleServer th v pk k_ = diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 05c461f6e..64447da23 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -81,7 +81,7 @@ import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -454,7 +454,7 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False @@ -466,7 +466,7 @@ testMatrix2 t runTest = do testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways) 3 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 0bf60afdd..af13ba030 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -35,21 +35,22 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do possibleAgentErrorType :: Gen AgentErrorType possibleAgentErrorType = arbitrary >>= \case - BROKER srv _ | skip srv -> discard - BROKER _ (RESPONSE e) | skip e -> discard - BROKER _ (UNEXPECTED e) | skip e -> discard + BROKER srv _ | hasSpaces srv -> discard SMP e | skipErrorType e -> discard NTF e | skipErrorType e -> discard - Agent.PROXY pxy srv _ | skip pxy || skip srv -> discard + Agent.PROXY pxy srv _ | hasSpaces pxy || hasSpaces srv -> discard Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard - Agent.PROXY _ _ (ProxyUnexpectedResponse e) | skip e -> discard + Agent.PROXY _ _ (ProxyUnexpectedResponse e) | hasUnicode e -> discard Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard ok -> pure ok - skip s = null s || any (\c -> c <= ' ' || c >= '\255') s + hasSpaces :: String -> Bool + hasSpaces = any (== ' ') + hasUnicode :: String -> Bool + hasUnicode = any (>= '\255') skipErrorType = \case SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e - SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> skip s - SMP.PROXY (SMP.BROKER (RESPONSE s)) -> skip s + SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> hasUnicode s + SMP.PROXY (SMP.BROKER (RESPONSE s)) -> hasUnicode s _ -> False deriving instance Generic AgentErrorType diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index b18b264e1..3cf09e5db 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -35,7 +35,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) @@ -199,8 +199,9 @@ initAgentServers = initAgentServers2 :: InitialAgentServers initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]} -initAgentServersProxy :: SMPProxyMode -> InitialAgentServers -initAgentServersProxy smpProxyMode = initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode}} +initAgentServersProxy :: SMPProxyMode -> SMPProxyFallback -> InitialAgentServers +initAgentServersProxy smpProxyMode smpProxyFallback = + initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode, smpProxyFallback}} agentCfg :: AgentConfig agentCfg = diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index e27970608..3d9d77033 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -118,10 +118,13 @@ cfg = cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +cfgV8 :: ServerConfig +cfgV8 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} + proxyCfg :: ServerConfig proxyCfg = cfgV7 - { allowSMPProxy = True, + { allowSMPProxy = True, smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}} } diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 52145a992..a9c0c5fb1 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -14,6 +14,7 @@ module SMPProxyTests where import AgentTests.FunctionalAPITests +import Control.Monad.Trans.Except (runExceptT) import Data.ByteString.Char8 (ByteString) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L @@ -87,10 +88,14 @@ smpProxyTests = do agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" it "first via proxy for unknown" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ + agentViaProxyVersionError where oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg testPort . const twoServers = twoServers_ proxyCfg proxyCfg - twoServersFirstProxy = twoServers_ proxyCfg cfgV7 + twoServersFirstProxy = twoServers_ proxyCfg cfgV8 twoServers_ cfg1 cfg2 runTest = withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest @@ -164,7 +169,18 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b baseId = 3 msgId = subtract baseId . fst aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} - servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode) {smp = userServers $ L.map noAuthSrv srvs} + servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + +agentViaProxyVersionError :: IO () +agentViaProxyVersionError = + withAgent 1 agentProxyCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do + Left (A.BROKER _ (TRANSPORT TEVersion)) <- + withAgent 2 agentProxyCfg (servers [SMPServer testHost testPort2 testKeyHash]) testDB2 $ \bob -> runExceptT $ do + (_bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + pure () + where + servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs} testNoProxy :: IO () testNoProxy = do diff --git a/tests/Test.hs b/tests/Test.hs index cd2b0d8c3..f9fb2a2c0 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -47,7 +47,7 @@ main = do $ do describe "Agent SQLite schema dump" schemaDumpTest describe "Core tests" $ do - xdescribe "Batching tests" batchingTests + describe "Batching tests" batchingTests describe "Encoding tests" encodingTests describe "Protocol error tests" protocolErrorTests describe "Version range" versionRangeTests From 7a07076277d69f701806c5233fe884efcc90e952 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Wed, 8 May 2024 02:06:09 +0300 Subject: [PATCH 019/125] transport: require ALPN for extended handshakes (#1134) * transport: require ALPN for extended handshakes * fix 8.10 build * rename --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/FileTransfer/Server/Env.hs | 17 ++------------- src/Simplex/Messaging/Agent/Server.hs | 2 +- src/Simplex/Messaging/Client.hs | 12 ++++++----- src/Simplex/Messaging/Notifications/Client.hs | 4 ++-- .../Messaging/Notifications/Server/Env.hs | 6 +++--- .../Messaging/Notifications/Server/Main.hs | 5 +++-- .../Messaging/Notifications/Transport.hs | 11 ++++++++-- src/Simplex/Messaging/Server/Env/STM.hs | 8 +++---- src/Simplex/Messaging/Server/Main.hs | 5 +++-- src/Simplex/Messaging/Transport.hs | 21 +++++++++++++++++-- .../Messaging/Transport/HTTP2/Server.hs | 2 +- src/Simplex/Messaging/Transport/Server.hs | 14 +++++++------ src/Simplex/Messaging/Transport/WebSockets.hs | 10 +++++++-- tests/AgentTests/FunctionalAPITests.hs | 17 +++++++++------ tests/NtfClient.hs | 5 ++++- tests/SMPClient.hs | 13 +++++++++--- 16 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/Simplex/FileTransfer/Server/Env.hs b/src/Simplex/FileTransfer/Server/Env.hs index b7f60c9af..414bfb4c4 100644 --- a/src/Simplex/FileTransfer/Server/Env.hs +++ b/src/Simplex/FileTransfer/Server/Env.hs @@ -13,12 +13,9 @@ import Control.Logger.Simple import Control.Monad import Control.Monad.IO.Unlift import Crypto.Random -import Data.Default (def) import Data.Int (Int64) -import Data.List (find) import Data.List.NonEmpty (NonEmpty) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) import Data.Time.Clock (getCurrentTime) import Data.Word (Word32) import Data.X509.Validation (Fingerprint (..)) @@ -103,7 +100,7 @@ supportedXFTPhandshakes :: [ALPN] supportedXFTPhandshakes = ["xftp/1"] newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv -newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile} = do +newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do random <- liftIO C.newRandom store <- atomically newFileStore storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile @@ -112,17 +109,7 @@ newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertifi forM_ fileSizeQuota $ \quota -> do logInfo $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used) when (quota < used) $ logInfo "WARNING: storage quota is less than used storage, no files can be uploaded!" - tlsServerParams' <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile - let TransportServerConfig {alpn} = transportConfig config - let tlsServerParams = case alpn of - Nothing -> tlsServerParams' - Just supported -> - tlsServerParams' - { T.serverHooks = - def - { T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported) - } - } + tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats} diff --git a/src/Simplex/Messaging/Agent/Server.hs b/src/Simplex/Messaging/Agent/Server.hs index 368c0a23d..da87fde11 100644 --- a/src/Simplex/Messaging/Agent/Server.hs +++ b/src/Simplex/Messaging/Agent/Server.hs @@ -49,7 +49,7 @@ runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, smpAgent :: forall c. Transport c => TProxy c -> ServiceName -> Env -> IO () smpAgent _ port env = do -- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation - tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile + tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile Nothing clientId <- newTVarIO initClientId runTransportServer started port tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 38c36f9f2..176602f4b 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -260,6 +260,7 @@ data ProtocolClientConfig v = ProtocolClientConfig defaultTransport :: (ServiceName, ATransport), -- | network configuration networkConfig :: NetworkConfig, + clientALPN :: Maybe [ALPN], -- | client-server protocol version range serverVRange :: VersionRange v, -- | agree shared session secret (used in SMP proxy) @@ -267,19 +268,20 @@ data ProtocolClientConfig v = ProtocolClientConfig } -- | Default protocol client configuration. -defaultClientConfig :: VersionRange v -> ProtocolClientConfig v -defaultClientConfig serverVRange = +defaultClientConfig :: Maybe [ALPN] -> VersionRange v -> ProtocolClientConfig v +defaultClientConfig clientALPN serverVRange = ProtocolClientConfig { qSize = 64, defaultTransport = ("443", transport @TLS), networkConfig = defaultNetworkConfig, + clientALPN, serverVRange, agreeSecret = False } {-# INLINE defaultClientConfig #-} defaultSMPClientConfig :: ProtocolClientConfig SMPVersion -defaultSMPClientConfig = defaultClientConfig supportedClientSMPRelayVRange +defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) supportedClientSMPRelayVRange {-# INLINE defaultSMPClientConfig #-} data Request err msg = Request @@ -332,7 +334,7 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId) -- A single queue can be used for multiple 'SMPClient' instances, -- as 'SMPServerTransmission' includes server information. getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmission v msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) -getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, serverVRange, agreeSecret} msgQ disconnected = do +getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do case chooseTransportHost networkConfig (host srv) of Right useHost -> (getCurrentTime >>= atomically . mkProtocolClient useHost >>= runClient useTransport useHost) @@ -370,7 +372,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) runClient (port', ATransport t) useHost c = do cVar <- newEmptyTMVarIO - let tcConfig = transportClientConfig networkConfig + let tcConfig = (transportClientConfig networkConfig) {alpn = clientALPN} username = proxyUsername transportSession action <- async $ diff --git a/src/Simplex/Messaging/Notifications/Client.hs b/src/Simplex/Messaging/Notifications/Client.hs index 72a92c278..cc698b344 100644 --- a/src/Simplex/Messaging/Notifications/Client.hs +++ b/src/Simplex/Messaging/Notifications/Client.hs @@ -10,7 +10,7 @@ import Data.Word (Word16) import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol -import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange) +import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes) import Simplex.Messaging.Protocol (ErrorType) import Simplex.Messaging.Util (bshow) @@ -19,7 +19,7 @@ type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse type NtfClientError = ProtocolClientError ErrorType defaultNTFClientConfig :: ProtocolClientConfig NTFVersion -defaultNTFClientConfig = defaultClientConfig supportedClientNTFVRange +defaultNTFClientConfig = defaultClientConfig (Just supportedNTFHandshakes) supportedClientNTFVRange ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519) ntfRegisterToken c pKey newTkn = diff --git a/src/Simplex/Messaging/Notifications/Server/Env.hs b/src/Simplex/Messaging/Notifications/Server/Env.hs index 5bcd72f3d..5ebd5230e 100644 --- a/src/Simplex/Messaging/Notifications/Server/Env.hs +++ b/src/Simplex/Messaging/Notifications/Server/Env.hs @@ -34,7 +34,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..)) -import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams) +import Simplex.Messaging.Transport.Server (TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) import UnliftIO.STM @@ -84,7 +84,7 @@ data NtfEnv = NtfEnv } newNtfServerEnv :: NtfServerConfig -> IO NtfEnv -newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile} = do +newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do random <- liftIO C.newRandom store <- atomically newNtfStore logInfo "restoring subscriptions..." @@ -92,7 +92,7 @@ newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsCo logInfo "restored subscriptions" subscriber <- atomically $ newNtfSubscriber subQSize smpAgentCfg random pushServer <- atomically $ newNtfPushServer pushQSize apnsConfig - tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile + tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile serverStats <- atomically . newNtfServerStats =<< liftIO getCurrentTime pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats} diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index a8d16d85d..0efb7d599 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -18,7 +18,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Server (runNtfServer) import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration) import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig) -import Simplex.Messaging.Notifications.Transport (supportedServerNTFVRange) +import Simplex.Messaging.Notifications.Transport (supportedNTFHandshakes, supportedServerNTFVRange) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Expiration @@ -133,7 +133,8 @@ ntfServerCLI cfgPath logPath = ntfServerVRange = supportedServerNTFVRange, transportConfig = defaultTransportServerConfig - { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini + { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, + alpn = Just supportedNTFHandshakes } } diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index e2c287437..342b42fc4 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -52,9 +52,15 @@ currentServerNTFVersion = VersionNTF 2 supportedClientNTFVRange :: VersionRangeNTF supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVersion +legacyServerNTFVRange :: VersionRangeNTF +legacyServerNTFVRange = mkVersionRange initialNTFVersion initialNTFVersion + supportedServerNTFVRange :: VersionRangeNTF supportedServerNTFVRange = mkVersionRange initialNTFVersion currentServerNTFVersion +supportedNTFHandshakes :: [ALPN] +supportedNTFHandshakes = ["ntf/1"] + type THandleNTF c p = THandle NTFVersion c p data NtfServerHandshake = NtfServerHandshake @@ -104,12 +110,13 @@ ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPa ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c let sk = C.signX509 serverSignKey $ C.publicToX509 k - sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange = ntfVRange, authPubKey = Just sk} + let ntfVersionRange = maybe legacyServerNTFVRange (const ntfVRange) $ getSessionALPN c + sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange, authPubKey = Just sk} getHandshake th >>= \case NtfClientHandshake {ntfVersion = v, keyHash} | keyHash /= kh -> throwError $ TEHandshake IDENTITY - | v `isCompatible` ntfVRange -> + | v `isCompatible` ntfVersionRange -> pure $ ntfThHandleServer th v pk | otherwise -> throwError $ TEHandshake VERSION diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index baadfc79b..6794ad979 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -33,8 +33,8 @@ import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, VersionSMP, VersionRangeSMP) -import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState) +import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP) +import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams, newSocketState) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) import UnliftIO.STM @@ -174,13 +174,13 @@ newSubscription subThread = do return Sub {subThread, delivered} newEnv :: ServerConfig -> IO Env -newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do +newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, transportConfig} = do server <- atomically newServer queueStore <- atomically newQueueStore msgStore <- atomically newMsgStore random <- liftIO C.newRandom storeLog <- restoreQueues queueStore `mapM` storeLogFile - tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile + tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) Fingerprint fp <- loadFingerprint caCertificateFile let serverIdentity = KeyHash fp serverStats <- atomically . newServerStats =<< getCurrentTime diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index a7844cc95..d75d02812 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -25,7 +25,7 @@ import Simplex.Messaging.Server (runSMPServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration) import Simplex.Messaging.Server.Expiration -import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange) +import Simplex.Messaging.Transport (simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) import Simplex.Messaging.Util (safeDecodeUtf8) @@ -211,7 +211,8 @@ smpServerCLI cfgPath logPath = smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig - { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini + { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, + alpn = Just supportedSMPHandshakes }, controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini } diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 8dfd15813..4b5098c39 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -33,9 +33,12 @@ module Simplex.Messaging.Transport VersionSMP, VersionRangeSMP, THandleSMP, + supportedSMPHandshakes, supportedClientSMPRelayVRange, supportedServerSMPRelayVRange, + legacyServerSMPRelayVRange, currentClientSMPRelayVersion, + legacyServerSMPRelayVersion, currentServerSMPRelayVersion, batchCmdsSMPVersion, basicAuthSMPVersion, @@ -152,6 +155,9 @@ authCmdsSMPVersion = VersionSMP 7 currentClientSMPRelayVersion :: VersionSMP currentClientSMPRelayVersion = VersionSMP 6 +legacyServerSMPRelayVersion :: VersionSMP +legacyServerSMPRelayVersion = VersionSMP 6 + currentServerSMPRelayVersion :: VersionSMP currentServerSMPRelayVersion = VersionSMP 7 @@ -160,9 +166,15 @@ currentServerSMPRelayVersion = VersionSMP 7 supportedClientSMPRelayVRange :: VersionRangeSMP supportedClientSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentClientSMPRelayVersion +legacyServerSMPRelayVRange :: VersionRangeSMP +legacyServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion legacyServerSMPRelayVersion + supportedServerSMPRelayVRange :: VersionRangeSMP supportedServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentServerSMPRelayVersion +supportedSMPHandshakes :: [ALPN] +supportedSMPHandshakes = ["smp/1"] + simplexMQVersion :: String simplexMQVersion = showVersion SMQ.version @@ -194,6 +206,9 @@ class Transport c where -- | tls-unique channel binding per RFC5929 tlsUnique :: c -> SessionId + -- | ALPN value negotiated for the session + getSessionALPN :: c -> Maybe ALPN + -- | Close connection closeConnection :: c -> IO () @@ -288,6 +303,7 @@ instance Transport TLS where getServerConnection = getTLS TServer getClientConnection = getTLS TClient getServerCerts = tlsServerCerts + getSessionALPN = tlsALPN tlsUnique = tlsUniq closeConnection tls = closeTLS $ tlsContext tls @@ -468,12 +484,13 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c sk = C.signX509 serverSignKey $ C.publicToX509 k certChain = getServerCerts c - sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = smpVRange, authPubKey = Just (certChain, sk)} + smpVersionRange = maybe legacyServerSMPRelayVRange (const smpVRange) $ getSessionALPN c + sendHandshake th $ ServerHandshake {sessionId, smpVersionRange, authPubKey = Just (certChain, sk)} getHandshake th >>= \case ClientHandshake {smpVersion = v, keyHash, authPubKey = k'} | keyHash /= kh -> throwE $ TEHandshake IDENTITY - | v `isCompatible` smpVRange -> + | v `isCompatible` smpVersionRange -> pure $ smpThHandleServer th v pk k' | otherwise -> throwE $ TEHandshake VERSION diff --git a/src/Simplex/Messaging/Transport/HTTP2/Server.hs b/src/Simplex/Messaging/Transport/HTTP2/Server.hs index c75d8fa31..f8ea1bd1d 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/Server.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/Server.hs @@ -51,7 +51,7 @@ data HTTP2Server = HTTP2Server -- This server is for testing only, it processes all requests in a single queue. getHTTP2Server :: HTTP2ServerConfig -> IO HTTP2Server getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, serverSupported, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do - tlsServerParams <- loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile + tlsServerParams <- loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile (alpn transportConfig) started <- newEmptyTMVarIO reqQ <- newTBQueueIO qSize action <- async $ diff --git a/src/Simplex/Messaging/Transport/Server.hs b/src/Simplex/Messaging/Transport/Server.hs index e7360b21b..145b438e0 100644 --- a/src/Simplex/Messaging/Transport/Server.hs +++ b/src/Simplex/Messaging/Transport/Server.hs @@ -28,10 +28,10 @@ import Control.Logger.Simple import Control.Monad import qualified Crypto.Store.X509 as SX import Data.Default (def) -import Data.List (find) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IM -import Data.Maybe (fromJust) +import Data.List (find) +import Data.Maybe (fromJust, fromMaybe) import qualified Data.X509 as X import Data.X509.Validation (Fingerprint (..)) import qualified Data.X509.Validation as XV @@ -152,12 +152,13 @@ startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted pure sock setStarted sock = atomically (tryPutTMVar started True) >> pure sock -loadTLSServerParams :: FilePath -> FilePath -> FilePath -> IO T.ServerParams +loadTLSServerParams :: FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams loadTLSServerParams = loadSupportedTLSServerParams supportedParameters -loadSupportedTLSServerParams :: T.Supported -> FilePath -> FilePath -> FilePath -> IO T.ServerParams -loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile = - fromCredential <$> loadServerCredential +loadSupportedTLSServerParams :: T.Supported -> FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams +loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile alpn_ = do + tlsServerParams <- fromCredential <$> loadServerCredential + pure tlsServerParams {T.serverHooks = maybe def alpnHooks alpn_} where loadServerCredential :: IO T.Credential loadServerCredential = @@ -172,6 +173,7 @@ loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile p T.serverHooks = def, T.serverSupported = serverSupported } + alpnHooks supported = def {T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported)} loadFingerprint :: FilePath -> IO Fingerprint loadFingerprint certificateFile = do diff --git a/src/Simplex/Messaging/Transport/WebSockets.hs b/src/Simplex/Messaging/Transport/WebSockets.hs index 062f4f0f0..0883fcc28 100644 --- a/src/Simplex/Messaging/Transport/WebSockets.hs +++ b/src/Simplex/Messaging/Transport/WebSockets.hs @@ -14,7 +14,8 @@ import Network.WebSockets import Network.WebSockets.Stream (Stream) import qualified Network.WebSockets.Stream as S import Simplex.Messaging.Transport - ( TProxy, + ( ALPN, + TProxy, Transport (..), TransportConfig (..), TransportError (..), @@ -28,6 +29,7 @@ import Simplex.Messaging.Transport.Buffer (trimCR) data WS = WS { wsPeer :: TransportPeer, tlsUniq :: ByteString, + wsALPN :: Maybe ALPN, wsStream :: Stream, wsConnection :: Connection, wsTransportConfig :: TransportConfig, @@ -61,6 +63,9 @@ instance Transport WS where getServerCerts :: WS -> X.CertificateChain getServerCerts = wsServerCerts + getSessionALPN :: WS -> Maybe ALPN + getSessionALPN = wsALPN + tlsUnique :: WS -> ByteString tlsUnique = tlsUniq @@ -90,7 +95,8 @@ getWS wsPeer cfg wsServerCerts cxt = withTlsUnique wsPeer cxt connectWS connectWS tlsUniq = do s <- makeTLSContextStream cxt wsConnection <- connectPeer wsPeer s - pure $ WS {wsPeer, tlsUniq, wsStream = s, wsConnection, wsTransportConfig = cfg, wsServerCerts} + wsALPN <- T.getNegotiatedProtocol cxt + pure $ WS {wsPeer, tlsUniq, wsALPN, wsStream = s, wsConnection, wsTransportConfig = cfg, wsServerCerts} connectPeer :: TransportPeer -> Stream -> IO Connection connectPeer TServer = acceptClientRequest connectPeer TClient = sendClientRequest diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index d3e4e6924..cdcf5baed 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -80,7 +80,7 @@ import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -90,7 +90,7 @@ import Simplex.Messaging.Protocol (BasicAuth, ErrorType (..), MsgBody, ProtocolS import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Server.Expiration -import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion) +import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion, supportedSMPHandshakes) import Simplex.Messaging.Util (diffToMicroseconds) import Simplex.Messaging.Version (VersionRange (..)) import qualified Simplex.Messaging.Version as V @@ -175,13 +175,16 @@ pattern Rcvd :: AgentMsgId -> ACommand 'Agent 'AEConn pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMsgId, msgRcptStatus = MROk}] smpCfgVPrev :: ProtocolClientConfig SMPVersion -smpCfgVPrev = (smpCfg agentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg agentCfg} +smpCfgVPrev = (smpCfg agentCfg) {clientALPN = Nothing, serverVRange = prevRange $ serverVRange $ smpCfg agentCfg} smpCfgV7 :: ProtocolClientConfig SMPVersion smpCfgV7 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +ntfCfgVPrev :: ProtocolClientConfig NTFVersion +ntfCfgVPrev = (ntfCfg agentCfg) {clientALPN = Nothing, serverVRange = V.mkVersionRange (VersionNTF 1) (VersionNTF 1)} + ntfCfgV2 :: ProtocolClientConfig NTFVersion -ntfCfgV2 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange (VersionNTF 1) authBatchCmdsNTFVersion} +ntfCfgV2 = (ntfCfg agentCfg) {serverVRange = V.mkVersionRange (VersionNTF 1) authBatchCmdsNTFVersion} agentCfgVPrev :: AgentConfig agentCfgVPrev = @@ -190,7 +193,8 @@ agentCfgVPrev = smpAgentVRange = prevRange $ smpAgentVRange agentCfg, smpClientVRange = prevRange $ smpClientVRange agentCfg, e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg, - smpCfg = smpCfgVPrev + smpCfg = smpCfgVPrev, + ntfCfg = ntfCfgVPrev } -- agent config for the next client version @@ -2416,7 +2420,8 @@ testCreateQueueAuth srvVersion clnt1 clnt2 = do where getClient clientId (clntAuth, clntVersion) db = let servers = initAgentServers {smp = userServers [ProtoServerWithAuth testSMPServer clntAuth]} - smpCfg = (defaultSMPClientConfig :: ProtocolClientConfig SMPVersion) {serverVRange = V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion} + alpn_ = if clntVersion >= authCmdsSMPVersion then Just supportedSMPHandshakes else Nothing + smpCfg = defaultClientConfig alpn_ $ V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion sndAuthAlg = if srvVersion >= authCmdsSMPVersion && clntVersion >= authCmdsSMPVersion then C.AuthAlg C.SX25519 else C.AuthAlg C.SEd25519 in getSMPAgentClient' clientId agentCfg {smpCfg, sndAuthAlg} servers db diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index 46a199777..564523e0b 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -36,6 +36,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Notifications.Protocol (NtfResponse) import Simplex.Messaging.Notifications.Server (runNtfServerBlocking) import Simplex.Messaging.Notifications.Server.Env +import qualified Simplex.Messaging.Notifications.Server.Env as Env import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Server.Push.APNS.Internal import Simplex.Messaging.Notifications.Transport @@ -45,6 +46,7 @@ import Simplex.Messaging.Transport.Client import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), http2TLSParams) import Simplex.Messaging.Transport.HTTP2.Server import Simplex.Messaging.Transport.Server +import qualified Simplex.Messaging.Transport.Server as Server import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec import UnliftIO.Async @@ -113,7 +115,8 @@ ntfServerCfgV2 :: NtfServerConfig ntfServerCfgV2 = ntfServerCfg { ntfServerVRange = mkVersionRange initialNTFVersion authBatchCmdsNTFVersion, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}} + smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}}, + Env.transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedNTFHandshakes} } withNtfServerStoreLog :: ATransport -> (ThreadId -> IO a) -> IO a diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index cf222c3b4..ae9baeb3c 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -24,8 +24,10 @@ import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client +import qualified Simplex.Messaging.Transport.Client as Client import Simplex.Messaging.Transport.Server -import Simplex.Messaging.Version (mkVersionRange) +import qualified Simplex.Messaging.Transport.Server as Server +import Simplex.Messaging.Version import System.Environment (lookupEnv) import System.Info (os) import Test.Hspec @@ -73,10 +75,15 @@ testSMPClient = testSMPClientVR supportedClientSMPRelayVRange testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a testSMPClientVR vr client = do Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost - runTransportClient defaultTransportClientConfig Nothing useHost testPort (Just testKeyHash) $ \h -> + let tcConfig = defaultTransportClientConfig {Client.alpn = clientALPN} + runTransportClient tcConfig Nothing useHost testPort (Just testKeyHash) $ \h -> runExceptT (smpClientHandshake h Nothing testKeyHash vr) >>= \case Right th -> client th Left e -> error $ show e + where + clientALPN + | authCmdsSMPVersion `isCompatible` vr = Just supportedSMPHandshakes + | otherwise = Nothing cfg :: ServerConfig cfg = @@ -104,7 +111,7 @@ cfg = privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt", smpServerVRange = supportedServerSMPRelayVRange, - transportConfig = defaultTransportServerConfig, + transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedSMPHandshakes}, controlPort = Nothing } From b40654c95dd4b00764f60bc3c35bc4e64b550cf1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 8 May 2024 13:05:06 +0100 Subject: [PATCH 020/125] update agent to v7/v2 SMP/NTF protocol versions (#997) * update agent to v7/v2 SMP/NTF protocol versions --- src/Simplex/Messaging/Notifications/Transport.hs | 2 +- src/Simplex/Messaging/Transport.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index 342b42fc4..58391c225 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -44,7 +44,7 @@ authBatchCmdsNTFVersion :: VersionNTF authBatchCmdsNTFVersion = VersionNTF 2 currentClientNTFVersion :: VersionNTF -currentClientNTFVersion = VersionNTF 1 +currentClientNTFVersion = VersionNTF 2 currentServerNTFVersion :: VersionNTF currentServerNTFVersion = VersionNTF 2 diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 4b5098c39..8c06c0d82 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -153,7 +153,7 @@ authCmdsSMPVersion :: VersionSMP authCmdsSMPVersion = VersionSMP 7 currentClientSMPRelayVersion :: VersionSMP -currentClientSMPRelayVersion = VersionSMP 6 +currentClientSMPRelayVersion = VersionSMP 7 legacyServerSMPRelayVersion :: VersionSMP legacyServerSMPRelayVersion = VersionSMP 6 From 3f57d54832de351249367212cd4a53fd260b4633 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 8 May 2024 16:57:04 +0400 Subject: [PATCH 021/125] xftp: catch exceptions in chunk download (#1133) --- src/Simplex/FileTransfer/Client.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 6cae2dd59..468df6157 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -245,8 +245,13 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec { let dhSecret = C.dh' sDhKey rpDhKey cbState <- liftEither . first PCECryptoError $ LC.cbInit dhSecret cbNonce let t = chunkTimeout config chunkSize - ExceptT (sequence <$> (t `timeout` download cbState)) >>= maybe (throwError PCEResponseTimeout) pure + ExceptT (sequence <$> (t `timeout` (download cbState `catches` errors))) >>= maybe (throwError PCEResponseTimeout) pure where + errors = + [ Handler $ \(_e :: H.HTTP2Error) -> pure $ Left PCENetworkError, + Handler $ \(e :: IOException) -> pure $ Left (PCEIOError e), + Handler $ \(_e :: SomeException) -> pure $ Left PCENetworkError + ] download cbState = runExceptT . withExceptT PCEResponseError $ receiveEncFile chunkPart cbState chunkSpec `catchError` \e -> From ea21b296fdbb7aa87d5d37f1181469492ec2daef Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 8 May 2024 15:33:51 +0100 Subject: [PATCH 022/125] agent: reset error count and do not report errors when consequitive timeouts happen while offline (#1136) * agent: reset error count and do not report errors when consequitive timeouts happen while offline * refactor * comment --- src/Simplex/Messaging/Agent/Client.hs | 16 ++++++++++------ src/Simplex/Messaging/Agent/Env/SQLite.hs | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 27223b12f..59170ce2d 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -630,12 +630,16 @@ reconnectSMPClient tc c tSess@(_, srv, _) qs = do let t = (length qs `div` 90 + 1) * tcpTimeout * 3 ExceptT (sequence <$> (t `timeout` runExceptT resubscribe)) >>= \case Just _ -> atomically $ writeTVar tc 0 - Nothing -> do - tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) - maxTC <- asks $ maxSubscriptionTimeouts . config - let err = if tc' >= maxTC then CRITICAL True else INTERNAL - msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess - atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ err msg) + Nothing -> + (offline <$> readTVarIO (userNetworkState c)) >>= \case + -- reset and do not report consequitive timeouts while offline + Just _ -> atomically $ writeTVar tc 0 + Nothing -> do + tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) + maxTC <- asks $ maxSubscriptionTimeouts . config + let err = if tc' >= maxTC then CRITICAL True else INTERNAL + msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess + atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ err msg) where resubscribe :: AM () resubscribe = do diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index f91144fdc..07d3f29a8 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -181,7 +181,7 @@ defaultAgentConfig = maxWorkerRestartsPerMin = 5, -- 3 consecutive subscription timeouts will result in alert to the user -- this is a fallback, as the timeout set to 3x of expected timeout, to avoid potential locking. - maxSubscriptionTimeouts = 3, + maxSubscriptionTimeouts = 5, storedMsgDataTTL = 21 * nominalDay, rcvFilesTTL = 2 * nominalDay, sndFilesTTL = nominalDay, From b27f126bab3d47212fd1f3a713aac3a6e3ed3062 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 8 May 2024 23:00:00 +0100 Subject: [PATCH 023/125] include server version range in transport handle (#1135) * include server version range in transport handle * xftp handshake * remove coment * simplify * comments --- src/Simplex/FileTransfer/Client.hs | 21 +++++---- src/Simplex/FileTransfer/Server.hs | 31 +++++++------ src/Simplex/FileTransfer/Server/Env.hs | 3 ++ src/Simplex/FileTransfer/Server/Main.hs | 2 + src/Simplex/Messaging/Agent/Protocol.hs | 1 + src/Simplex/Messaging/Client.hs | 1 + src/Simplex/Messaging/Crypto/Ratchet.hs | 31 +++++++------ .../Messaging/Notifications/Transport.hs | 44 +++++++++++------- src/Simplex/Messaging/Transport.hs | 46 ++++++++++++------- src/Simplex/Messaging/Version.hs | 23 ++++++++++ tests/CoreTests/BatchingTests.hs | 1 + tests/CoreTests/VersionRangeTests.hs | 36 +++++++++++++++ tests/XFTPClient.hs | 2 + 13 files changed, 173 insertions(+), 69 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 4efff9388..90067e22d 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -57,7 +57,7 @@ import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client import Simplex.Messaging.Transport.HTTP2.File import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM) -import Simplex.Messaging.Version (compatibleVersion, pattern Compatible) +import Simplex.Messaging.Version import UnliftIO import UnliftIO.Directory @@ -109,7 +109,9 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected let HTTP2Client {sessionId, sessionALPN} = http2Client - thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True} + v = VersionXFTP 1 + thServerVRange = versionToRange v + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True} logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN thParams@THandleParams {thVersion} <- case sessionALPN of Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 @@ -123,9 +125,10 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpClientHandshakeV1 :: VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient) xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake - (v, sk) <- processServerHandshake shs + (vr, sk) <- processServerHandshake shs + let v = maxVersion vr sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash} - pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v} + pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v, thServerVRange = vr} where getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake getServerHandshake = do @@ -133,13 +136,13 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing liftHS . smpDecode =<< liftHS (C.unPad shsBody) - processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519) + processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION - case xftpVersionRange `compatibleVersion` serverVRange of - Nothing -> throwError $ PCEResponseError HANDSHAKE - Just (Compatible v) -> - fmap (v,) . liftHS $ do + case xftpVersionRange `compatibleVRange` serverVRange of + Nothing -> throwError $ PCETransportError TEVersion + Just (Compatible vr) -> + fmap (vr,) . liftHS $ do let (X.CertificateChain cert, exact) = serverAuth case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 7b6787a43..41c652ea2 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -63,7 +63,7 @@ import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize) import Simplex.Messaging.Transport.HTTP2.Server import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials) import Simplex.Messaging.Util -import Simplex.Messaging.Version (isCompatible) +import Simplex.Messaging.Version import System.Exit (exitFailure) import System.FilePath (()) import System.IO (hPrint, hPutStrLn, universalNewlineMode) @@ -91,10 +91,10 @@ runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpSer data Handshake = HandshakeSent C.PrivateKeyX25519 - | HandshakeAccepted (THandleAuth 'TServer) VersionXFTP + | HandshakeAccepted (THandleParams XFTPVersion 'TServer) xftpServer :: XFTPServerConfig -> TMVar Bool -> M () -xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do +xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do mapM_ (expireServerFiles Nothing) fileExpiration restoreServerStats raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer @@ -111,7 +111,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira let cleanup sessionId = atomically $ TM.delete sessionId sessions liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do reqBody <- getHTTP2Body r xftpBlockSize - let thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True} + let v = VersionXFTP 1 + thServerVRange = versionToRange v + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True} req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse} flip runReaderT env $ case sessionALPN of Nothing -> processRequest req0 @@ -121,12 +123,12 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here) _ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer)) - xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do + xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams0@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do s <- atomically $ TM.lookup sessionId sessions r <- runExceptT $ case s of Nothing -> processHello Just (HandshakeSent pk) -> processClientHandshake pk - Just (HandshakeAccepted auth v) -> pure $ Just thParams {thAuth = Just auth, thVersion = v} + Just (HandshakeAccepted thParams) -> pure $ Just thParams either sendError pure r where processHello = do @@ -134,21 +136,24 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira (k, pk) <- atomically . C.generateKeyPair =<< asks random atomically $ TM.insert sessionId (HandshakeSent pk) sessions let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k) - let hs = XFTPServerHandshake {xftpVersionRange = supportedFileServerVRange, sessionId, authPubKey} + let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey} shs <- encodeXftp hs liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing processClientHandshake pk = do unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE body <- liftHS $ C.unPad bodyHead - XFTPClientHandshake {xftpVersion, keyHash} <- liftHS $ smpDecode body + XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ smpDecode body kh <- asks serverIdentity unless (keyHash == kh) $ throwError HANDSHAKE - unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE - let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} - atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions - liftIO . sendResponse $ H.responseNoBody N.ok200 [] - pure Nothing + case compatibleVRange' xftpServerVRange v of + Just (Compatible vr) -> do + let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} + thParams = thParams0 {thAuth = Just auth, thVersion = v, thServerVRange = vr} + atomically $ TM.insert sessionId (HandshakeAccepted thParams) sessions + liftIO . sendResponse $ H.responseNoBody N.ok200 [] + pure Nothing + Nothing -> throwError HANDSHAKE sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) sendError err = do runExceptT (encodeXftp err) >>= \case diff --git a/src/Simplex/FileTransfer/Server/Env.hs b/src/Simplex/FileTransfer/Server/Env.hs index 414bfb4c4..58c1393f3 100644 --- a/src/Simplex/FileTransfer/Server/Env.hs +++ b/src/Simplex/FileTransfer/Server/Env.hs @@ -25,6 +25,7 @@ import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId) import Simplex.FileTransfer.Server.Stats import Simplex.FileTransfer.Server.Store import Simplex.FileTransfer.Server.StoreLog +import Simplex.FileTransfer.Transport (VersionRangeXFTP) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey) import Simplex.Messaging.Server.Expiration @@ -61,6 +62,8 @@ data XFTPServerConfig = XFTPServerConfig caCertificateFile :: FilePath, privateKeyFile :: FilePath, certificateFile :: FilePath, + -- | XFTP client-server protocol version range + xftpServerVRange :: VersionRangeXFTP, -- stats config - see SMP server config logStatsInterval :: Maybe Int64, logStatsStartTime :: Int64, diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index d53b3f4fa..b909b1617 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -20,6 +20,7 @@ import Simplex.FileTransfer.Chunks import Simplex.FileTransfer.Description (FileSize (..)) import Simplex.FileTransfer.Server (runXFTPServer) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern XFTPServer) @@ -174,6 +175,7 @@ xftpServerCLI cfgPath logPath = do caCertificateFile = c caCrtFile, privateKeyFile = c serverKeyFile, certificateFile = c serverCrtFile, + xftpServerVRange = supportedFileServerVRange, logStatsInterval = logStats $> 86400, -- seconds logStatsStartTime = 0, -- seconds from 00:00 UTC serverStatsLogFile = combine logPath "file-server-stats.daily.log", diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 5cfd7af03..7160863f1 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -1302,6 +1302,7 @@ instance VersionRangeI SMPClientVersion SMPQueueUri where type VersionT SMPClientVersion SMPQueueUri = SMPQueueInfo versionRange = clientVRange toVersionT (SMPQueueUri _vr addr) v = SMPQueueInfo v addr + toVersionRange (SMPQueueUri _vr addr) vr = SMPQueueUri vr addr -- | SMP queue information sent out-of-band. -- diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 6b1beb6a1..59fbd4c12 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -172,6 +172,7 @@ smpClientStub g sessionId thVersion thAuth = do THandleParams { sessionId, thVersion, + thServerVRange = supportedServerSMPRelayVRange, thAuth, blockSize = smpBlockSize, implySessId = thVersion >= authCmdsSMPVersion, diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index 6ab84aa30..db13fddc1 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -117,7 +117,7 @@ import Simplex.Messaging.Crypto.SNTRUP761.Bindings import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, parseE, parseE') -import Simplex.Messaging.Util ((<$?>), ($>>=)) +import Simplex.Messaging.Util (($>>=), (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import UnliftIO.STM @@ -266,6 +266,7 @@ instance VersionRangeI E2EVersion (E2ERatchetParamsUri s a) where type VersionT E2EVersion (E2ERatchetParamsUri s a) = (E2ERatchetParams s a) versionRange (E2ERatchetParamsUri vr _ _ _) = vr toVersionT (E2ERatchetParamsUri _ k1 k2 kem_) v = E2ERatchetParams v k1 k2 kem_ + toVersionRange (E2ERatchetParamsUri _ k1 k2 kem_) vr = E2ERatchetParamsUri vr k1 k2 kem_ type RcvE2ERatchetParamsUri a = E2ERatchetParamsUri 'RKSProposed a @@ -377,13 +378,15 @@ generateE2EParams g v useKEM_ = do where kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s)) kemParams = case useKEM_ of - Just useKem | v >= pqRatchetE2EEncryptVersion -> Just <$> do - ks@(k, _) <- sntrup761Keypair g - case useKem of - ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) - AcceptKEM k' -> do - (ct, shared) <- sntrup761Enc g k' - pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) + Just useKem + | v >= pqRatchetE2EEncryptVersion -> + Just <$> do + ks@(k, _) <- sntrup761Keypair g + case useKem of + ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) + AcceptKEM k' -> do + (ct, shared) <- sntrup761Enc g k' + pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) _ -> pure Nothing -- used by party initiating connection, Bob in double-ratchet spec @@ -456,7 +459,7 @@ pqX3dh (sk1, rk1) dh1 dh2 dh3 kemAccepted = pq = maybe "" (\RatchetKEMAccepted {rcPQRss = KEMSharedKey ss} -> BA.convert ss) kemAccepted (hk, nhk, sk) = let salt = B.replicate 64 '\0' - in hkdf3 salt dhs "SimpleXX3DH" + in hkdf3 salt dhs "SimpleXX3DH" type RatchetX448 = Ratchet 'X448 @@ -698,8 +701,8 @@ data EncMessageHeader = EncMessageHeader -- this encoding depends on version in EncMessageHeader because it is "current" ratchet version instance Encoding EncMessageHeader where - smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} - = smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody + smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} = + smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody smpP = do (ehVersion, ehIV, ehAuthTag) <- smpP ehBody <- largeP @@ -708,8 +711,6 @@ instance Encoding EncMessageHeader where -- the encoder always uses 2-byte lengths for the new version, even for short headers without PQ keys. encodeLarge :: VersionE2E -> ByteString -> ByteString encodeLarge v s - -- the condition for length is not necessary, it's here as a fallback. - -- | v >= pqRatchetE2EEncryptVersion || B.length s > 255 = smpEncode $ Large s | v >= pqRatchetE2EEncryptVersion = smpEncode $ Large s | otherwise = smpEncode s @@ -729,8 +730,8 @@ data EncRatchetMessage = EncRatchetMessage } encodeEncRatchetMessage :: VersionE2E -> EncRatchetMessage -> ByteString -encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} - = encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) +encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} = + encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) encRatchetMessageP :: Parser EncRatchetMessage encRatchetMessageP = do diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index 7a3efee54..ddb8880b5 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -117,9 +117,10 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do NtfClientHandshake {ntfVersion = v, keyHash} | keyHash /= kh -> throwError $ TEHandshake IDENTITY - | v `isCompatible` ntfVersionRange -> - pure $ ntfThHandleServer th v pk - | otherwise -> throwE TEVersion + | otherwise -> + case compatibleVRange' ntfVersionRange v of + Just (Compatible vr) -> pure $ ntfThHandleServer th v vr pk + Nothing -> throwE TEVersion -- | Notifcations server client transport handshake. ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) @@ -128,34 +129,45 @@ ntfClientHandshake c keyHash ntfVRange = do NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th if sessionId /= sessId then throwError TEBadSession - else case ntfVersionRange `compatibleVersion` ntfVRange of - Just (Compatible v) -> do + else case ntfVersionRange `compatibleVRange` ntfVRange of + Just (Compatible vr) -> do ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey signedKey (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) + let v = maxVersion vr sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash} - pure $ ntfThHandleClient th v ck_ + pure $ ntfThHandleClient th v vr ck_ Nothing -> throwE TEVersion -ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer -ntfThHandleServer th v pk = +ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> VersionRangeNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer +ntfThHandleServer th v vr pk = let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} - in ntfThHandle_ th v (Just thAuth) + in ntfThHandle_ th v vr (Just thAuth) -ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient -ntfThHandleClient th v ck_ = +ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> VersionRangeNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient +ntfThHandleClient th v vr ck_ = let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = Nothing}) <$> ck_ - in ntfThHandle_ th v thAuth + in ntfThHandle_ th v vr thAuth -ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p -ntfThHandle_ th@THandle {params} v thAuth = +ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> VersionRangeNTF -> Maybe (THandleAuth p) -> THandleNTF c p +ntfThHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional let v3 = v >= authBatchCmdsNTFVersion - params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} + params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v3, batch = v3} in (th :: THandleNTF c p) {params = params'} ntfTHandle :: Transport c => c -> THandleNTF c p ntfTHandle c = THandle {connection = c, params} where - params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False} + v = VersionNTF 0 + params = + THandleParams + { sessionId = tlsUnique c, + blockSize = ntfBlockSize, + thVersion = v, + thServerVRange = versionToRange v, + thAuth = Nothing, + implySessId = False, + batch = False + } diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index ffdbf4a20..ad5a29822 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -340,6 +340,8 @@ type THandleSMP c p = THandle SMPVersion c p data THandleParams v p = THandleParams { sessionId :: SessionId, blockSize :: Int, + -- | server protocol version range + thServerVRange :: VersionRange v, -- | agreed server protocol version thVersion :: Version v, -- | peer public key for command authorization and shared secrets for entity ID encryption @@ -493,9 +495,10 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do ClientHandshake {smpVersion = v, keyHash, authPubKey = k'} | keyHash /= kh -> throwE $ TEHandshake IDENTITY - | v `isCompatible` smpVersionRange -> - pure $ smpThHandleServer th v pk k' - | otherwise -> throwE TEVersion + | otherwise -> + case compatibleVRange' smpVersionRange v of + Just (Compatible vr) -> pure $ smpThHandleServer th v vr pk k' + Nothing -> throwE TEVersion -- | Client SMP transport handshake. -- @@ -506,8 +509,8 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th if sessionId /= sessId then throwE TEBadSession - else case smpVersionRange `compatibleVersion` smpVRange of - Just (Compatible v) -> do + else case smpVersionRange `compatibleVRange` smpVRange of + Just (Compatible vr) -> do ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do case cert of @@ -516,24 +519,25 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey exact (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) + let v = maxVersion vr sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} - pure $ smpThHandleClient th v (snd <$> ks_) ck_ + pure $ smpThHandleClient th v vr (snd <$> ks_) ck_ Nothing -> throwE TEVersion -smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer -smpThHandleServer th v pk k_ = +smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpThHandleServer th v vr pk k_ = let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_} - in smpThHandle_ th v (Just thAuth) + in smpThHandle_ th v vr (Just thAuth) -smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient -smpThHandleClient th v pk_ ck_ = +smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpThHandleClient th v vr pk_ ck_ = let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_ - in smpThHandle_ th v thAuth + in smpThHandle_ th v vr thAuth -smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p -smpThHandle_ th@THandle {params} v thAuth = +smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpThHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional - let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} + let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion} in (th :: THandleSMP c p) {params = params'} sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () @@ -546,7 +550,17 @@ getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP smpTHandle :: Transport c => c -> THandleSMP c p smpTHandle c = THandle {connection = c, params} where - params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True} + v = VersionSMP 0 + params = + THandleParams + { sessionId = tlsUnique c, + blockSize = smpBlockSize, + thServerVRange = versionToRange v, + thVersion = v, + thAuth = Nothing, + implySessId = False, + batch = True + } $(J.deriveJSON (sumTypeJSON id) ''HandshakeError) diff --git a/src/Simplex/Messaging/Version.hs b/src/Simplex/Messaging/Version.hs index 25f7368d1..5576cfa9f 100644 --- a/src/Simplex/Messaging/Version.hs +++ b/src/Simplex/Messaging/Version.hs @@ -23,6 +23,8 @@ module Simplex.Messaging.Version isCompatibleRange, proveCompatible, compatibleVersion, + compatibleVRange, + compatibleVRange', ) where @@ -98,6 +100,7 @@ class VersionScope v => VersionI v a | a -> v where class VersionScope v => VersionRangeI v a | a -> v where type VersionT v a versionRange :: a -> VersionRange v + toVersionRange :: a -> VersionRange v -> a toVersionT :: a -> Version v -> VersionT v a instance VersionScope v => VersionI v (Version v) where @@ -108,6 +111,7 @@ instance VersionScope v => VersionI v (Version v) where instance VersionScope v => VersionRangeI v (VersionRange v) where type VersionT v (VersionRange v) = Version v versionRange = id + toVersionRange _ vr = vr toVersionT _ v = v newtype Compatible a = Compatible_ a @@ -135,5 +139,24 @@ compatibleVersion x vr = max1 = maxVersion $ versionRange x max2 = maxVersion vr +-- | intersection of version ranges +compatibleVRange :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible a) +compatibleVRange x vr = + compatibleVRange_ x (max min1 min2) (min max1 max2) + where + VRange min1 max1 = versionRange x + VRange min2 max2 = vr + +-- | version range capped by compatible version +compatibleVRange' :: VersionRangeI v a => a -> Version v -> Maybe (Compatible a) +compatibleVRange' x v + | v <= max1 = compatibleVRange_ x min1 v + | otherwise = Nothing + where + VRange min1 max1 = versionRange x + +compatibleVRange_ :: VersionRangeI v a => a -> Version v -> Version v -> Maybe (Compatible a) +compatibleVRange_ x v1 v2 = Compatible_ . toVersionRange x <$> safeVersionRange v1 v2 + mkCompatibleIf :: a -> Bool -> Maybe (Compatible a) x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index 6350baa91..caab0637a 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -325,6 +325,7 @@ testTHandleParams v sessionId = { sessionId, blockSize = smpBlockSize, thVersion = v, + thServerVRange = supportedServerSMPRelayVRange, thAuth = Nothing, implySessId = v >= authCmdsSMPVersion, batch = True diff --git a/tests/CoreTests/VersionRangeTests.hs b/tests/CoreTests/VersionRangeTests.hs index cef556376..ff53cc6ca 100644 --- a/tests/CoreTests/VersionRangeTests.hs +++ b/tests/CoreTests/VersionRangeTests.hs @@ -6,6 +6,7 @@ module CoreTests.VersionRangeTests where +import Data.Word (Word16) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.Messaging.Version @@ -38,6 +39,28 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do (vr 1 3, vr 2 3) `compatible` Just (Version 3) (vr 1 3, vr 2 4) `compatible` Just (Version 3) (vr 1 2, vr 3 4) `compatible` Nothing + it "should choose mutually compatible version range (range intersection)" $ do + (vr 1 1, vr 1 1) `compatibleVR` Just (vr 1 1) + (vr 1 1, vr 1 2) `compatibleVR` Just (vr 1 1) + (vr 1 2, vr 1 2) `compatibleVR` Just (vr 1 2) + (vr 1 2, vr 2 3) `compatibleVR` Just (vr 2 2) + (vr 1 3, vr 2 3) `compatibleVR` Just (vr 2 3) + (vr 1 3, vr 2 4) `compatibleVR` Just (vr 2 3) + (vr 1 2, vr 3 4) `compatibleVR` Nothing + it "should choose compatible version range with changed max version (capped range)" $ do + (vr 1 1, 1) `compatibleVR'` Just (vr 1 1) + (vr 1 1, 2) `compatibleVR'` Nothing + (vr 1 2, 2) `compatibleVR'` Just (vr 1 2) + (vr 1 2, 3) `compatibleVR'` Nothing + (vr 1 3, 2) `compatibleVR'` Just (vr 1 2) + (vr 1 3, 3) `compatibleVR'` Just (vr 1 3) + (vr 1 3, 4) `compatibleVR'` Nothing + (vr 2 3, 1) `compatibleVR'` Nothing + (vr 2 3, 2) `compatibleVR'` Just (vr 2 2) + (vr 2 3, 3) `compatibleVR'` Just (vr 2 3) + (vr 2 4, 1) `compatibleVR'` Nothing + (vr 2 4, 3) `compatibleVR'` Just (vr 2 3) + (vr 2 4, 4) `compatibleVR'` Just (vr 2 4) it "should check if version is compatible" $ do isCompatible @T (Version 1) (vr 1 2) `shouldBe` True isCompatible @T (Version 2) (vr 1 2) `shouldBe` True @@ -63,3 +86,16 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do case compatibleVersion vr1 vr2 of Just (Compatible v') -> Just v' `shouldBe` v Nothing -> Nothing `shouldBe` v + compatibleVR :: (VersionRange T, VersionRange T) -> Maybe (VersionRange T) -> Expectation + (vr1, vr2) `compatibleVR` vr' = do + (vr1, vr2) `checkCompatibleVR` vr' + (vr2, vr1) `checkCompatibleVR` vr' + (vr1, vr2) `checkCompatibleVR` vr' = + case compatibleVRange vr1 vr2 of + Just (Compatible vr'') -> Just vr'' `shouldBe` vr' + Nothing -> Nothing `shouldBe` vr' + compatibleVR' :: (VersionRange T, Word16) -> Maybe (VersionRange T) -> Expectation + (vr1, v2) `compatibleVR'` vr' = + case compatibleVRange' vr1 (Version v2) of + Just (Compatible vr'') -> Just vr'' `shouldBe` vr' + Nothing -> Nothing `shouldBe` vr' diff --git a/tests/XFTPClient.hs b/tests/XFTPClient.hs index 5f38cc639..0152625a8 100644 --- a/tests/XFTPClient.hs +++ b/tests/XFTPClient.hs @@ -14,6 +14,7 @@ import Simplex.FileTransfer.Client import Simplex.FileTransfer.Description import Simplex.FileTransfer.Server (runXFTPServerBlocking) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import Simplex.Messaging.Protocol (XFTPServer) import Simplex.Messaging.Transport (ALPN) import Simplex.Messaging.Transport.Server @@ -118,6 +119,7 @@ testXFTPServerConfig_ alpn = caCertificateFile = "tests/fixtures/ca.crt", privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt", + xftpServerVRange = supportedFileServerVRange, logStatsInterval = Nothing, logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", From 5cafd9d5c494cbc2d70c07e43aef31ac92f9883b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 9 May 2024 09:20:57 +0100 Subject: [PATCH 024/125] server: more efficient responses to batch subscriptions (#1137) * server: more efficient responses to batch subscriptions * comments * comment * enable tests * LogError --- src/Simplex/Messaging/Server.hs | 32 ++++++++++++++++++-------- tests/AgentTests.hs | 14 +++++------ tests/AgentTests/FunctionalAPITests.hs | 28 +++++++++++++++++++--- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 5415ebd65..895ba28ae 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -53,7 +53,8 @@ import Data.Either (fromRight, partitionEithers) import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM -import Data.List (intercalate) +import Data.List (intercalate, mapAccumR) +import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (isNothing) @@ -482,16 +483,29 @@ send :: Transport c => THandleSMP c 'TServer -> Client -> IO () send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send" forever $ do - ts <- atomically $ L.sortWith tOrder <$> readTBQueue sndQ - -- TODO we can authorize responses as well - void . liftIO . tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts + sendTransmissions =<< atomically (readTBQueue sndQ) atomically . writeTVar sndActiveAt =<< liftIO getSystemTime where - tOrder :: Transmission BrokerMsg -> Int - tOrder (_, _, cmd) = case cmd of - MSG {} -> 0 - NMSG {} -> 0 - _ -> 1 + sendTransmissions :: NonEmpty (Transmission BrokerMsg) -> IO () + sendTransmissions ts + | L.length ts <= 2 = tSend ts + | otherwise = do + let (msgs, ts') = mapAccumR splitMessages [] ts + -- If the request had batched subscriptions (L.length ts > 2) + -- this will reply OK to all SUBs in the first batched transmission, + -- to reduce client timeouts. + tSend ts' + -- After that all messages will be sent in separate transmissions, + -- without any client response timeouts. + mapM_ tSend (L.nonEmpty msgs) + where + splitMessages :: [Transmission BrokerMsg] -> Transmission BrokerMsg -> ([Transmission BrokerMsg], Transmission BrokerMsg) + splitMessages msgs t@(corrId, entId, cmd) = case cmd of + -- replace MSG response with OK, accumulating MSG in a separate list. + MSG {} -> ((CorrId "", entId, cmd) : msgs, (corrId, entId, OK)) + _ -> (msgs, t) + tSend :: NonEmpty (Transmission BrokerMsg) -> IO () + tSend = void . tPut h . L.map (\t -> Right (Nothing, encodeTransmission params t)) disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index b890c2c00..32610b54e 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -27,9 +27,9 @@ import GHC.Stack (withFrozenCallStack) import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) -import Simplex.Messaging.Agent.Protocol hiding (MID, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (CONF, INFO, MID, REQ) import qualified Simplex.Messaging.Agent.Protocol as A -import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOn, pattern IKPQOff, pattern PQEncOn, pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ErrorType (..)) @@ -547,10 +547,10 @@ testResumeDeliveryQuotaExceeded _ alice bob = do bob <#= \case ("", "alice", Msg "message 4") -> True; _ -> False bob #: ("4", "alice", "ACK 7") #> ("4", "alice", OK) inAnyOrder - (tGetAgent alice) - [ \case ("", c, Right (SENT 8)) -> c == "bob"; _ -> False, - \case ("", c, Right QCONT) -> c == "bob"; _ -> False - ] + (tGetAgent alice) + [ \case ("", c, Right (SENT 8)) -> c == "bob"; _ -> False, + \case ("", c, Right QCONT) -> c == "bob"; _ -> False + ] bob <#= \case ("", "alice", Msg "over quota") -> True; _ -> False -- message 8 is skipped because of alice agent sending "QCONT" message bob #: ("5", "alice", "ACK 9") #> ("5", "alice", OK) @@ -580,7 +580,7 @@ enableKEMStr _ = "" pqConnModeStr :: InitialKeys -> ByteString pqConnModeStr (IKNoPQ PQSupportOff) = "" -pqConnModeStr pq = " " <> strEncode pq +pqConnModeStr pq = " " <> strEncode pq sendMessage :: Transport c => (c, ConnId) -> (c, ConnId) -> ByteString -> IO () sendMessage (h1, name1) (h2, name2) msg = do diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index cdcf5baed..59e433ea5 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -58,10 +58,10 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (isRight) import Data.Int (Int64) -import Data.List (nub) +import Data.List (find, nub) import Data.List.NonEmpty (NonEmpty) import qualified Data.Map as M -import Data.Maybe (isNothing) +import Data.Maybe (isJust, isNothing) import qualified Data.Set as S import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.Clock.System (SystemTime (..), getSystemTime) @@ -337,6 +337,9 @@ functionalAPITests t = do skip "faster version of the previous test (200 subscriptions gets very slow with test coverage)" $ it "should subscribe to multiple (6) subscriptions with batching" $ testBatchedSubscriptions 6 3 t + it "should subscribe to multiple connections with pending messages" $ + withSmpServer t $ + testBatchedPendingMessages 10 5 describe "Async agent commands" $ do it "should connect using async agent commands" $ withSmpServer t testAsyncCommands @@ -1534,7 +1537,7 @@ testBatchedSubscriptions :: Int -> Int -> ATransport -> IO () testBatchedSubscriptions nCreate nDel t = withAgentClientsCfgServers2 agentCfg agentCfg initAgentServers2 $ \a b -> do conns <- runServers $ do - conns <- replicateM (nCreate :: Int) $ makeConnection_ PQSupportOff a b + conns <- replicateM nCreate $ makeConnection_ PQSupportOff a b forM_ conns $ \(aId, bId) -> exchangeGreetings_ PQEncOff a bId b aId let (aIds', bIds') = unzip $ take nDel conns delete a bIds' @@ -1593,6 +1596,25 @@ testBatchedSubscriptions nCreate nDel t = killThread t1 pure res +testBatchedPendingMessages :: Int -> Int -> IO () +testBatchedPendingMessages nCreate nMsgs = + withA $ \a -> do + conns <- withB $ \b -> runRight $ do + replicateM nCreate $ makeConnection a b + let msgConns = take nMsgs conns + runRight_ $ forM_ msgConns $ \(_, bId) -> sendMessage a bId SMP.noMsgFlags "hello" + replicateM_ nMsgs $ get a =##> \case ("", cId, SENT _) -> isJust $ find ((cId ==) . snd) msgConns; _ -> False + withB $ \b -> runRight_ $ do + r <- subscribeConnections b $ map fst conns + liftIO $ all isRight r `shouldBe` True + replicateM_ nMsgs $ do + ("", cId, Msg' msgId _ "hello") <- get b + liftIO $ isJust (find ((cId ==) . fst) msgConns) `shouldBe` True + ackMessage b cId msgId Nothing + where + withA = withAgent 1 agentCfg initAgentServers testDB + withB = withAgent 2 agentCfg initAgentServers testDB2 + testAsyncCommands :: IO () testAsyncCommands = withAgentClients2 $ \alice bob -> runRight_ $ do From b48215d341f6b7425fa108b88d1ec11990c66ba5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 9 May 2024 15:36:02 +0100 Subject: [PATCH 025/125] proxy: additional configuration for SOCKS proxy usage in SMP proxy client (#1138) * proxy: additional configuration for SOCKS proxy usage in SMP proxy client * update --- src/Simplex/FileTransfer/Client.hs | 6 ++-- src/Simplex/Messaging/Client.hs | 29 ++++++++++++++---- src/Simplex/Messaging/Server/Main.hs | 45 +++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 2e0585e30..7875542a6 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -99,11 +99,11 @@ defaultXFTPClientConfig = getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do - let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = clientALPN} - http2Config = xftpHTTP2Config tcConfig config - username = proxyUsername transportSession + let username = proxyUsername transportSession ProtocolServer _ host port keyHash = srv useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host + let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN} + http2Config = xftpHTTP2Config tcConfig config clientVar <- newTVarIO Nothing let usePort = if null port then "443" else port clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 59fbd4c12..c931aefc2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -69,6 +69,7 @@ module Simplex.Messaging.Client NetworkConfig (..), TransportSessionMode (..), HostMode (..), + SocksMode (..), SMPProxyMode (..), SMPProxyFallback (..), defaultClientConfig, @@ -214,10 +215,20 @@ data HostMode HMPublic deriving (Eq, Show) +data SocksMode + = -- | always use SOCKS proxy when enabled + SMAlways + | -- | use SOCKS proxy only for .onion hosts when no public host is available + -- This mode is used in SMP proxy to minimize SOCKS proxy usage. + SMOnion + deriving (Eq, Show) + -- | network configuration for the client data NetworkConfig = NetworkConfig { -- | use SOCKS5 proxy socksProxy :: Maybe SocksProxy, + -- | when to use SOCKS proxy + socksMode :: SocksMode, -- | determines critera which host is chosen from the list hostMode :: HostMode, -- | if above criteria is not met, if the below setting is True return error, otherwise use the first host @@ -267,6 +278,7 @@ defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig { socksProxy = Nothing, + socksMode = SMAlways, hostMode = HMOnionViaSocks, requiredHostMode = False, sessionMode = TSMUser, @@ -282,9 +294,14 @@ defaultNetworkConfig = logTLSErrors = False } -transportClientConfig :: NetworkConfig -> TransportClientConfig -transportClientConfig NetworkConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} = - TransportClientConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} +transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig +transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host = + TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} + where + useSocksProxy SMAlways = socksProxy + useSocksProxy SMOnion = case host of + THOnionHost _ -> socksProxy + _ -> Nothing {-# INLINE transportClientConfig #-} -- | protocol client configuration. @@ -298,7 +315,7 @@ data ProtocolClientConfig v = ProtocolClientConfig clientALPN :: Maybe [ALPN], -- | client-server protocol version range serverVRange :: VersionRange v, - -- | agree shared session secret (used in SMP proxy) + -- | agree shared session secret (used in SMP proxy for additional encryption layer) agreeSecret :: Bool } @@ -411,7 +428,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) runClient (port', ATransport t) useHost c = do cVar <- newEmptyTMVarIO - let tcConfig = (transportClientConfig networkConfig) {alpn = clientALPN} + let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN} username = proxyUsername transportSession action <- async $ @@ -994,6 +1011,8 @@ authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_ $(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SM") ''SocksMode) + $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 39f753af0..418ef36c8 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -14,11 +14,12 @@ import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Maybe (fromMaybe) +import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Network.Socket (HostName) import Options.Applicative -import Simplex.Messaging.Client (ProtocolClientConfig (..)) +import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig) import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String @@ -132,7 +133,7 @@ smpServerCLI cfgPath logPath = ) <> "\n\n\ \# control_port_admin_password:\n\ - \# control_port_user_password:\n\ + \# control_port_user_password:\n\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" <> ("host: " <> host <> "\n") @@ -140,6 +141,18 @@ smpServerCLI cfgPath logPath = <> "log_tls_errors: off\n\ \websockets: off\n\ \# control_port: 5224\n\n\ + \[PROXY]\n\ + \# Network configuration for SMP proxy client.\n\ + \# `host_mode` can be 'public' (default) or 'onion'.\n\ + \# It defines prefferred hostname for destination servers with multiple hostnames.\n\ + \# host_mode: public\n\ + \# required_host_mode: off\n\n\ + \# SOCKS proxy port for forwarding messages to destination servers.\n\ + \# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\ + \# socks_proxy: localhost:9050\n\n\ + \# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\ + \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ + \# socks_mode: onion\n\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" @@ -218,9 +231,33 @@ smpServerCLI cfgPath logPath = alpn = Just supportedSMPHandshakes }, controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, agreeSecret = True}}, - allowSMPProxy = True -- TODO: "get from INI" + smpAgentCfg = + defaultSMPClientAgentConfig + { smpCfg = + (smpCfg defaultSMPClientAgentConfig) + { serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, + agreeSecret = True, + networkConfig = + defaultNetworkConfig + { socksProxy = either error id <$> strDecodeIni "PROXY" "socks_proxy" ini, + socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini, + hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini, + requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini + } + } + }, + allowSMPProxy = True } + textToSocksMode :: Text -> SocksMode + textToSocksMode = \case + "always" -> SMAlways + "onion" -> SMOnion + s -> error . T.unpack $ "Invalid socks_mode: " <> s + textToHostMode :: Text -> HostMode + textToHostMode = \case + "public" -> HMPublic + "onion" -> HMOnionViaSocks + s -> error . T.unpack $ "Invalid host_mode: " <> s data CliCommand = Init InitOptions From b7afb725fd5cdfde67a82303535aea0367121023 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 May 2024 10:55:19 +0100 Subject: [PATCH 026/125] proxy: send MWARN event to user on server version or host more errors (#1140) * proxy: include delivery path in SENT event * send MWARN event to user on server version or host more errors * Revert "proxy: include delivery path in SENT event" This reverts commit 5c476718ec84f4b9f7e2eca6a7ac7c3b260c0275. --- src/Simplex/Messaging/Agent.hs | 6 +++++- src/Simplex/Messaging/Agent/Client.hs | 26 ++++++++++++++----------- src/Simplex/Messaging/Agent/Protocol.hs | 7 +++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3a0e13157..be0d35cb4 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1358,7 +1358,11 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork | temporaryOrHostError e -> do let msgTimeout = if msgType == AM_HELLO_ then helloTimeout else messageTimeout expireTs <- addUTCTime (-msgTimeout) <$> liftIO getCurrentTime - if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RIFast + if internalTs < expireTs + then notifyDelMsgs msgId e expireTs + else do + when (serverHostError e) $ notify $ MWARN (unId msgId) e + retrySndMsg RIFast | otherwise -> notifyDel msgId err where retrySndMsg riMode = do diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 133fae96a..f2c43fc67 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -46,6 +46,7 @@ module Simplex.Messaging.Agent.Client sendInvitation, temporaryAgentError, temporaryOrHostError, + serverHostError, secureQueue, enableQueueNotifications, enableQueuesNtfs, @@ -616,7 +617,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq Left e -> do liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e atomically $ do - unless (persistentProxyError e) $ do + unless (serverHostError e) $ do removeSessVar rv destSrv prs TM.delete destSess smpProxiedRelays putTMVar (sessionVar rv) (Left e) @@ -1060,7 +1061,7 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do case r of Right r' -> pure r' Left e - | persistentProxyError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) + | serverHostError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) | otherwise -> throwE e sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> @@ -1303,17 +1304,20 @@ temporaryAgentError = \case _ -> False temporaryOrHostError :: AgentErrorType -> Bool -temporaryOrHostError = \case - BROKER _ HOST -> True - SMP (SMP.PROXY (SMP.BROKER HOST)) -> True - PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER HOST))) -> True - e -> temporaryAgentError e +temporaryOrHostError e = temporaryAgentError e || serverHostError e +{-# INLINE temporaryOrHostError #-} -persistentProxyError :: AgentErrorType -> Bool -persistentProxyError = \case - BROKER _ (SMP.TRANSPORT TEVersion) -> True - SMP (SMP.PROXY (SMP.BROKER (SMP.TRANSPORT TEVersion))) -> True +serverHostError :: AgentErrorType -> Bool +serverHostError = \case + BROKER _ e -> brokerHostError e + SMP (SMP.PROXY (SMP.BROKER e)) -> brokerHostError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerHostError e _ -> False + where + brokerHostError = \case + HOST -> True + SMP.TRANSPORT TEVersion -> True + _ -> False -- | Subscribe to queues. The list of results can have a different order. subscribeQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 7160863f1..a18f64064 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -396,6 +396,7 @@ data ACommand (p :: AParty) (e :: AEntity) where SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn + MWARN :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn @@ -459,6 +460,7 @@ data ACommandTag (p :: AParty) (e :: AEntity) where SEND_ :: ACommandTag Client AEConn MID_ :: ACommandTag Agent AEConn SENT_ :: ACommandTag Agent AEConn + MWARN_ :: ACommandTag Agent AEConn MERR_ :: ACommandTag Agent AEConn MERRS_ :: ACommandTag Agent AEConn MSG_ :: ACommandTag Agent AEConn @@ -515,6 +517,7 @@ aCommandTag = \case SEND {} -> SEND_ MID {} -> MID_ SENT {} -> SENT_ + MWARN {} -> MWARN_ MERR {} -> MERR_ MERRS {} -> MERRS_ MSG {} -> MSG_ @@ -1668,6 +1671,7 @@ instance StrEncoding ACmdTag where "SEND" -> t SEND_ "MID" -> ct MID_ "SENT" -> ct SENT_ + "MWARN" -> ct MWARN_ "MERR" -> ct MERR_ "MERRS" -> ct MERRS_ "MSG" -> ct MSG_ @@ -1726,6 +1730,7 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where SEND_ -> "SEND" MID_ -> "MID" SENT_ -> "SENT" + MWARN_ -> "MWARN" MERR_ -> "MERR" MERRS_ -> "MERRS" MSG_ -> "MSG" @@ -1797,6 +1802,7 @@ commandP binaryP = RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP) MID_ -> s (MID <$> A.decimal <*> _strP) SENT_ -> s (SENT <$> A.decimal <*> _strP) + MWARN_ -> s (MWARN <$> A.decimal <* A.space <*> strP) MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP) MERRS_ -> s (MERRS <$> strP_ <*> strP) MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP) @@ -1860,6 +1866,7 @@ serializeCommand = \case SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody] MID mId pqEnc -> s (MID_, mId, pqEnc) SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_) + MWARN mId e -> s (MWARN_, mId, e) MERR mId e -> s (MERR_, mId, e) MERRS mIds e -> s (MERRS_, mIds, e) MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody] From dc111437fda043b4921cf31f6e402a9644ad3324 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 May 2024 15:15:43 +0100 Subject: [PATCH 027/125] 5.7.3.0 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 7a536f7bf..093293e80 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.2.0 +version: 5.7.3.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 7539d675a..808e52ae0 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.2.0 +version: 5.7.3.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 727fd8b8f5eed729ee82f25f831e86df502b27b8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 May 2024 22:19:11 +0100 Subject: [PATCH 028/125] server: more efficient response to batched subscriptions (#1141) * server: more efficient response to batched subscriptions * add sndMsgQ for interleaving messages with replies * remove redundant liftIO * refactor * refactor2 * rename * fix * diff * remove comment * remove comment --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> --- src/Simplex/Messaging/Server.hs | 50 +++++++++++++++---------- src/Simplex/Messaging/Server/Env/STM.hs | 4 +- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 895ba28ae..85beccd6b 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -411,7 +411,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do hPutStrLn h "AUTH" runClientTransport :: Transport c => THandleSMP c 'TServer -> M () -runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do +runClientTransport h@THandle {params = THandleParams {thVersion, sessionId}} = do q <- asks $ tbqSize . config ts <- liftIO getSystemTime active <- asks clients @@ -422,11 +422,12 @@ runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = pure new s <- asks server expCfg <- asks $ inactiveClientExpiration . config + th <- newMVar h -- put TH under a fair lock to interleave messages and command responses labelMyThread . B.unpack $ "client $" <> encode sessionId - raceAny_ ([liftIO $ send th c, client c s, receive th c] <> disconnectThread_ c expCfg) + raceAny_ ([liftIO $ send th c, liftIO $ sendMsg th c, client c s, receive h c] <> disconnectThread_ c expCfg) `finally` clientDisconnected c where - disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport th (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)] + disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport h (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)] disconnectThread_ _ _ = [] noSubscriptions c = atomically $ (&&) <$> TM.null (subscriptions c) <*> TM.null (ntfSubscriptions c) @@ -459,10 +460,10 @@ cancelSub sub = _ -> return () receive :: Transport c => THandleSMP c 'TServer -> Client -> M () -receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do +receive h@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive" forever $ do - ts <- L.toList <$> liftIO (tGet th) + ts <- L.toList <$> liftIO (tGet h) atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime as <- partitionEithers <$> mapM cmdAction ts write sndQ $ fst as @@ -479,33 +480,41 @@ receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActi VRFailed -> Left (corrId, queueId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty -send :: Transport c => THandleSMP c 'TServer -> Client -> IO () -send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do +send :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO () +send th c@Client {sndQ, msgQ, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send" - forever $ do - sendTransmissions =<< atomically (readTBQueue sndQ) - atomically . writeTVar sndActiveAt =<< liftIO getSystemTime + forever $ atomically (readTBQueue sndQ) >>= sendTransmissions where sendTransmissions :: NonEmpty (Transmission BrokerMsg) -> IO () sendTransmissions ts - | L.length ts <= 2 = tSend ts + | L.length ts <= 2 = tSend th c ts | otherwise = do - let (msgs, ts') = mapAccumR splitMessages [] ts + let (msgs_, ts') = mapAccumR splitMessages [] ts -- If the request had batched subscriptions (L.length ts > 2) -- this will reply OK to all SUBs in the first batched transmission, -- to reduce client timeouts. - tSend ts' + tSend th c ts' -- After that all messages will be sent in separate transmissions, - -- without any client response timeouts. - mapM_ tSend (L.nonEmpty msgs) + -- without any client response timeouts, and allowing them to interleave + -- with other requests responses. + mapM_ (atomically . writeTBQueue msgQ) $ L.nonEmpty msgs_ where splitMessages :: [Transmission BrokerMsg] -> Transmission BrokerMsg -> ([Transmission BrokerMsg], Transmission BrokerMsg) splitMessages msgs t@(corrId, entId, cmd) = case cmd of -- replace MSG response with OK, accumulating MSG in a separate list. MSG {} -> ((CorrId "", entId, cmd) : msgs, (corrId, entId, OK)) _ -> (msgs, t) - tSend :: NonEmpty (Transmission BrokerMsg) -> IO () - tSend = void . tPut h . L.map (\t -> Right (Nothing, encodeTransmission params t)) + +sendMsg :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO () +sendMsg th c@Client {msgQ, sessionId} = do + labelMyThread . B.unpack $ "client $" <> encode sessionId <> " sendMsg" + forever $ atomically (readTBQueue msgQ) >>= mapM_ (\t -> tSend th c [t]) + +tSend :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> NonEmpty (Transmission BrokerMsg) -> IO () +tSend th Client {sndActiveAt} ts = do + withMVar th $ \h@THandle {params} -> + void . tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts + atomically . writeTVar sndActiveAt =<< liftIO getSystemTime disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do @@ -989,9 +998,10 @@ saveServerMessages keepMsgs = asks (storeMsgsFile . config) >>= mapM_ saveMessag >>= mapM_ (B.hPutStrLn h . strEncode . MLRv3 rId) restoreServerMessages :: M Int -restoreServerMessages = asks (storeMsgsFile . config) >>= \case - Just f -> ifM (doesFileExist f) (restoreMessages f) (pure 0) - Nothing -> pure 0 +restoreServerMessages = + asks (storeMsgsFile . config) >>= \case + Just f -> ifM (doesFileExist f) (restoreMessages f) (pure 0) + Nothing -> pure 0 where restoreMessages f = do logInfo $ "restoring messages from file " <> T.pack f diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 6794ad979..bd8262f07 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -128,6 +128,7 @@ data Client = Client ntfSubscriptions :: TMap NotifierId (), rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)), sndQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), + msgQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), endThreads :: TVar (IntMap (Weak ThreadId)), endThreadSeq :: TVar Int, thVersion :: VersionSMP, @@ -161,12 +162,13 @@ newClient nextClientId qSize thVersion sessionId createdAt = do ntfSubscriptions <- TM.empty rcvQ <- newTBQueue qSize sndQ <- newTBQueue qSize + msgQ <- newTBQueue qSize endThreads <- newTVar IM.empty endThreadSeq <- newTVar 0 connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do From acc7faea11453e291393440816a060098faabf86 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 May 2024 22:24:00 +0100 Subject: [PATCH 029/125] 5.7.3.1 --- CHANGELOG.md | 15 +++++++++++++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5792195d..f0093ba92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 5.7.3 + +SMP/NTF protocol: +- add ALPN for handshake version negotiation, similar to XFTP (to preserve backwards compatibility with the old clients). +- upgrade clients to versions v7/v2 of the protocols. + +SMP server: +- faster responses to subscription requests. + +XFTP client: +- fix network exception during file download treated as permanent file error. + +SMP agent: +- do not report subscription timeouts while client is offline. + # 5.7.2 SMP agent: diff --git a/package.yaml b/package.yaml index 093293e80..6d63ad488 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.3.0 +version: 5.7.3.1 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 808e52ae0..de10da8b8 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.3.0 +version: 5.7.3.1 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 8516b0dd5b8bdee600fdcfca43d6e15b97bbf1c0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 11 May 2024 17:11:28 +0100 Subject: [PATCH 030/125] proxy: negotiate client-relay version, include it in PFWD commands and in encrypted forwarded transmissions (#1144) * proxy: negotiate client-relay version, include it in PFWD commands and in encrypted forwarded transmissions * rename * inline * comment * use correct server version when encoding forwarded commands --- src/Simplex/Messaging/Client.hs | 17 +++++----- src/Simplex/Messaging/Protocol.hs | 17 +++++----- src/Simplex/Messaging/Server.hs | 25 +++++++------- src/Simplex/Messaging/Server/Env/STM.hs | 6 ++-- src/Simplex/Messaging/Transport.hs | 44 ++++++++++++++++++------- 5 files changed, 65 insertions(+), 44 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index c931aefc2..92dd4047b 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -819,11 +819,10 @@ proxySMPMessage :: MsgFlags -> MsgBody -> ExceptT SMPClientError IO (Either ProxyClientError ()) --- TODO use version -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId _v serverKey) spKey sId flags msg = do +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams - serverThParams = proxyThParams {sessionId, thAuth = serverThAuth} + serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} (cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g let cmdSecret = C.dh' serverKey cmdPrivKey nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g @@ -837,13 +836,13 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength -- proxy interaction errors are wrapped - tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et))) >>= \case + tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case Right r -> case r of PRES (EncResponse er) -> do -- server interaction errors are thrown directly t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er - case tParse proxyThParams t' of - t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of + case tParse serverThParams t' of + t'' :| [] -> case tDecodeParseValidate serverThParams t'' of (_auth, _signed, (_c, _e, cmd)) -> case cmd of Right OK -> pure $ Right () Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay @@ -862,15 +861,15 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c -- sends RFWD :: EncFwdTransmission -> Command Sender -- receives RRES :: EncFwdResponse -> BrokerMsg -- proxy should send PRES to the client with EncResponse -forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse -forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do +forwardSMPMessage :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of Nothing -> throwError $ PCETransportError TENoServerAuth Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap - let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission} + let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission} eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 250c76fcf..1812dce37 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -251,7 +251,7 @@ maxMessageLength v | otherwise = 16088 -- 16064 - always use this size to determine allowed ranges paddedProxiedMsgLength :: Int -paddedProxiedMsgLength = 16244 -- 16241 .. 16245 +paddedProxiedMsgLength = 16242 -- 16241 .. 16243 -- TODO v6.0 change to 16064 type MaxMessageLen = 16088 @@ -399,7 +399,7 @@ data Command (p :: Party) where -- - 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 :: C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy + 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 @@ -434,16 +434,17 @@ newtype EncTransmission = EncTransmission ByteString data FwdTransmission = FwdTransmission { fwdCorrId :: CorrId, + fwdVersion :: VersionSMP, fwdKey :: C.PublicKeyX25519, fwdTransmission :: EncTransmission } instance Encoding FwdTransmission where - smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} = - smpEncode (corrId, fwdKey, Tail t) + smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} = + smpEncode (corrId, fwdVersion, fwdKey, Tail t) smpP = do - (corrId, fwdKey, Tail t) <- smpP - pure FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} + (corrId, fwdVersion, fwdKey, Tail t) <- smpP + pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} newtype EncFwdTransmission = EncFwdTransmission ByteString deriving (Show) @@ -1278,7 +1279,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where PING -> e PING_ NSUB -> e NSUB_ PRXY host auth_ -> e (PRXY_, ' ', host, auth_) - PFWD pubKey (EncTransmission s) -> e (PFWD_, ' ', pubKey, Tail s) + PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s) RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s) where e :: Encoding a => a -> ByteString @@ -1346,7 +1347,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP) CT SProxiedClient tag -> Cmd SProxiedClient <$> case tag of - PFWD_ -> PFWD <$> _smpP <*> (EncTransmission . unTail <$> smpP) + PFWD_ -> PFWD <$> _smpP <*> smpP <*> (EncTransmission . unTail <$> smpP) PRXY_ -> PRXY <$> _smpP <*> smpP CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 156f5da18..85a9073bc 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -89,6 +89,7 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Buffer (trimCR) import Simplex.Messaging.Transport.Server import Simplex.Messaging.Util +import Simplex.Messaging.Version import System.Exit (exitFailure) import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode) import System.Mem.Weak (deRefWeak) @@ -650,20 +651,21 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi proxyResp = \case Left err -> ERR $ smpProxyError err Right smp -> - let THandleParams {sessionId = srvSessId, thVersion, thAuth} = thParams smp - vr = supportedServerSMPRelayVRange -- TODO this should be destination relay version range - in if thVersion >= sendingProxySMPVersion - then case thAuth of + let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp + in case compatibleVRange thServerVRange proxiedSMPRelayVRange of + -- Cap the destination relay version range to prevent client version fingerprinting. + -- See comment for proxiedSMPRelayVersion. + Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey Nothing -> ERR $ transportErr TENoServerAuth - else ERR $ transportErr TEVersion - PFWD pubKey encBlock -> do + _ -> ERR $ transportErr TEVersion + PFWD fwdV pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case Just smp | v >= sendingProxySMPVersion -> liftIO $ either (ERR . smpProxyError) PRES <$> - runExceptT (forwardSMPMessage smp corrId pubKey encBlock) `catchError` (pure . Left . PCEIOError) + runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catchError` (pure . Left . PCEIOError) | otherwise -> pure . ERR $ transportErr TEVersion where THandleParams {thVersion = v} = thParams smp @@ -952,13 +954,14 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret' let proxyNonce = C.cbNonce $ bs corrId s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s - FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' + FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' let clientSecret = C.dh' fwdKey serverPrivKey clientNonce = C.cbNonce $ bs fwdCorrId b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et + let clntTHParams = smpTHParamsSetVersion fwdVersion thParams' -- only allowing single forwarded transactions - t' <- case tParse thParams' b of - t :| [] -> pure $ tDecodeParseValidate thParams' t + t' <- case tParse clntTHParams b of + t :| [] -> pure $ tDecodeParseValidate clntTHParams t _ -> throwE BLOCK let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} -- process forwarded SEND @@ -972,7 +975,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi _ -> pure (corrId', entId', ERR $ CMD PROHIBITED) -- encode response - r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of + r' <- case batchTransmissions (batch clntTHParams) (blockSize clntTHParams) [Right (Nothing, encodeTransmission clntTHParams r)] of [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right TBError _ _ : _ -> throwE BLOCK TBTransmission b' _ : _ -> pure b' diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 845d483b1..f23192aeb 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -146,8 +146,7 @@ data Client = Client connected :: TVar Bool, createdAt :: SystemTime, rcvActiveAt :: TVar SystemTime, - sndActiveAt :: TVar SystemTime, - proxyClient_ :: TVar (Maybe C.DhSecretX25519) -- this client is actually an SMP proxy + sndActiveAt :: TVar SystemTime } data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub @@ -179,8 +178,7 @@ newClient nextClientId qSize thVersion sessionId createdAt = do connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - proxyClient_ <- newTVar Nothing - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, proxyClient_} + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index f12168840..561e9c4de 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -36,6 +36,7 @@ module Simplex.Messaging.Transport supportedSMPHandshakes, supportedClientSMPRelayVRange, supportedServerSMPRelayVRange, + proxiedSMPRelayVRange, legacyServerSMPRelayVRange, currentClientSMPRelayVersion, legacyServerSMPRelayVersion, @@ -77,6 +78,7 @@ module Simplex.Messaging.Transport tGetBlock, sendHandshake, getHandshake, + smpTHParamsSetVersion, ) where @@ -155,13 +157,21 @@ sendingProxySMPVersion :: VersionSMP sendingProxySMPVersion = VersionSMP 8 currentClientSMPRelayVersion :: VersionSMP -currentClientSMPRelayVersion = VersionSMP 7 +currentClientSMPRelayVersion = VersionSMP 8 legacyServerSMPRelayVersion :: VersionSMP legacyServerSMPRelayVersion = VersionSMP 6 currentServerSMPRelayVersion :: VersionSMP -currentServerSMPRelayVersion = VersionSMP 7 +currentServerSMPRelayVersion = VersionSMP 8 + +-- Max SMP protocol version to be used in e2e encrypted +-- connection between client and server, as defined by SMP proxy. +-- SMP proxy sets it to lower than its current version +-- to prevent client version fingerprinting by the +-- destination relays when clients upgrade at different times. +proxiedSMPRelayVersion :: VersionSMP +proxiedSMPRelayVersion = VersionSMP 8 -- minimal supported protocol version is 4 -- TODO remove code that supports sending commands without batching @@ -174,6 +184,10 @@ legacyServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion legacyServerSMPR supportedServerSMPRelayVRange :: VersionRangeSMP supportedServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentServerSMPRelayVersion +-- This range initially allows only version 8 - see the comment above. +proxiedSMPRelayVRange :: VersionRangeSMP +proxiedSMPRelayVRange = mkVersionRange sendingProxySMPVersion proxiedSMPRelayVersion + supportedSMPHandshakes :: [ALPN] supportedSMPHandshakes = ["smp/1"] @@ -497,7 +511,7 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do throwE $ TEHandshake IDENTITY | otherwise -> case compatibleVRange' smpVersionRange v of - Just (Compatible vr) -> pure $ smpThHandleServer th v vr pk k' + Just (Compatible vr) -> pure $ smpTHandleServer th v vr pk k' Nothing -> throwE TEVersion -- | Client SMP transport handshake. @@ -521,25 +535,31 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) let v = maxVersion vr sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} - pure $ smpThHandleClient th v vr (snd <$> ks_) ck_ + pure $ smpTHandleClient th v vr (snd <$> ks_) ck_ Nothing -> throwE TEVersion -smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer -smpThHandleServer th v vr pk k_ = +smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpTHandleServer th v vr pk k_ = let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_} - in smpThHandle_ th v vr (Just thAuth) + in smpTHandle_ th v vr (Just thAuth) -smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient -smpThHandleClient th v vr pk_ ck_ = +smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpTHandleClient th v vr pk_ ck_ = let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_ - in smpThHandle_ th v vr thAuth + in smpTHandle_ th v vr thAuth -smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p -smpThHandle_ th@THandle {params} v vr thAuth = +smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpTHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion} in (th :: THandleSMP c p) {params = params'} +-- This function is only used with v >= 8, so currently it's a simple record update. +-- It may require some parameters update in the future, to be consistent with smpTHandle_. +smpTHParamsSetVersion :: VersionSMP -> THandleParams SMPVersion p -> THandleParams SMPVersion p +smpTHParamsSetVersion v params = params {thVersion = v} +{-# INLINE smpTHParamsSetVersion #-} + sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () sendHandshake th = ExceptT . tPutBlock th . smpEncode From 103ae06d554340bc9af87209e2244faf1f7d5c9a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 11 May 2024 23:38:27 +0100 Subject: [PATCH 031/125] agent: remove critical error on subscription timeout (#1146) --- src/Simplex/Messaging/Agent/Client.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 59170ce2d..cfed6b932 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -637,9 +637,9 @@ reconnectSMPClient tc c tSess@(_, srv, _) qs = do Nothing -> do tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) maxTC <- asks $ maxSubscriptionTimeouts . config - let err = if tc' >= maxTC then CRITICAL True else INTERNAL - msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess - atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ err msg) + when (tc' >= maxTC) $ do + let msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess + atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL msg) where resubscribe :: AM () resubscribe = do From 1339a8da1105ae836b3804113498dabbdf5901ee Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 11 May 2024 23:39:28 +0100 Subject: [PATCH 032/125] 5.7.4.0 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 6d63ad488..7ba64f531 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.3.1 +version: 5.7.4.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index de10da8b8..19c99acc4 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.3.1 +version: 5.7.4.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 91cc48aabe6f490a0829b7601fe024edf7a1216c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 12 May 2024 17:47:08 +0100 Subject: [PATCH 033/125] agent: do not mark subscriptions on expired sessions as active, mark delayed subscriptions as active on the same session, do not cancel sending expired commands (#1127) * agent: do not mark subscriptions on expired sessions as active, do mark delayed subscriptions as active on the same session, SUBOK response in the next SMP protocol version * client: prevent sub actions from zombie sessions (#1122) * client: prevent sub actions from zombie sessions * error handling * add AERR to pass background errors to client * switch to activeClientSession * put closeClient under activeClientSession * rename * remove AERR, do not skip processing * move check and state update to one transaction * catch extra UPs * fix * check queue is still pending before making it active --------- Co-authored-by: Evgeny Poberezkin * do not forward agent error * revert not expiring sending subs * fixes * track subscription responses better * add pending connection * Revert "revert not expiring sending subs" This reverts commit 4310a69391fc791015d86a7b8449156b5e441ffa. * do not expire sending commands * rename * fix race * function --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> --- package.yaml | 1 + simplexmq.cabal | 14 +- src/Simplex/FileTransfer/Agent.hs | 11 +- src/Simplex/Messaging/Agent.hs | 77 ++++++---- src/Simplex/Messaging/Agent/Client.hs | 119 ++++++++++----- src/Simplex/Messaging/Agent/Protocol.hs | 38 ++--- src/Simplex/Messaging/Client.hs | 140 ++++++++++-------- src/Simplex/Messaging/Client/Agent.hs | 4 +- src/Simplex/Messaging/Notifications/Server.hs | 12 +- src/Simplex/Messaging/Server.hs | 5 +- src/Simplex/Messaging/Session.hs | 4 + src/Simplex/Messaging/Transport.hs | 2 +- tests/AgentTests.hs | 8 +- tests/AgentTests/FunctionalAPITests.hs | 36 ++--- tests/AgentTests/NotificationTests.hs | 21 +-- tests/CoreTests/ProtocolErrorTests.hs | 3 + tests/XFTPAgent.hs | 20 ++- 17 files changed, 312 insertions(+), 203 deletions(-) diff --git a/package.yaml b/package.yaml index 7ba64f531..02a088e23 100644 --- a/package.yaml +++ b/package.yaml @@ -180,6 +180,7 @@ ghc-options: - -Wall - -Wcompat - -Werror=incomplete-patterns + - -Werror=missing-methods - -Wredundant-constraints - -Wincomplete-record-updates - -Wincomplete-uni-patterns diff --git a/simplexmq.cabal b/simplexmq.cabal index 19c99acc4..3366cb0b8 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -174,7 +174,7 @@ library src default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 include-dirs: cbits c-sources: @@ -255,7 +255,7 @@ executable ntf-server apps/ntf-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -330,7 +330,7 @@ executable smp-agent apps/smp-agent default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -405,7 +405,7 @@ executable smp-server apps/smp-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -480,7 +480,7 @@ executable xftp apps/xftp default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -555,7 +555,7 @@ executable xftp-server apps/xftp-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -662,7 +662,7 @@ test-suite simplexmq-test tests default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1 + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1 build-depends: HUnit ==1.6.* , QuickCheck ==2.14.* diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index d04117942..415ead6c0 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -262,9 +262,9 @@ runXFTPRcvLocalWorker c Worker {doWork} = do withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting chunkPaths <- getChunkPaths chunks encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths - when (FileSize encSize /= size) $ throwError $ XFTP XFTP.SIZE + when (FileSize encSize /= size) $ throwError $ XFTP "" XFTP.SIZE encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths - when (FileDigest encDigest /= digest) $ throwError $ XFTP XFTP.DIGEST + when (FileDigest encDigest /= digest) $ throwError $ XFTP "" XFTP.DIGEST let destFile = CryptoFile fsSavePath cfArgs void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile case redirect of @@ -281,10 +281,11 @@ runXFTPRcvLocalWorker c Worker {doWork} = do -- proceed with redirect yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath) next@FileDescription {chunks = nextChunks} <- case strDecode (LB.toStrict yaml) of - Left _ -> throwError . XFTP $ XFTP.REDIRECT "decode error" + -- TODO switch to another error constructor + Left _ -> throwError . XFTP "" $ XFTP.REDIRECT "decode error" Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest}) - | dstSize /= redirectSize -> throwError . XFTP $ XFTP.REDIRECT "size mismatch" - | dstDigest /= redirectDigest -> throwError . XFTP $ XFTP.REDIRECT "digest mismatch" + | dstSize /= redirectSize -> throwError . XFTP "" $ XFTP.REDIRECT "size mismatch" + | dstDigest /= redirectDigest -> throwError . XFTP "" $ XFTP.REDIRECT "digest mismatch" | otherwise -> pure fd -- register and download chunks from the actual file withStore c $ \db -> updateRcvFileRedirect db redirectDbId next diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index f93d03e35..f834927d8 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -158,7 +158,7 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission) +import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission, TransmissionType (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs) import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) @@ -734,11 +734,9 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv (SCMContact, CR.IKUsePQ) -> throwError $ CMD PROHIBITED _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - (rq, qUri) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e rq' <- withStore c $ \db -> updateNewConnRcv db connId rq - liftIO $ case subMode of - SMOnlyCreate -> pure () - SMSubscribe -> addSubscription c rq' + lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId when enableNtfs $ do ns <- asks ntfSupervisor atomically $ sendNtfSubCommand ns (connId, NSCCreate) @@ -863,12 +861,10 @@ joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do - (rq, qUri) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode let qInfo = toVersionT qUri smpClientVersion rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq - liftIO $ case subMode of - SMOnlyCreate -> pure () - SMSubscribe -> addSubscription c rq' + lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId when enableNtfs $ do ns <- asks ntfSupervisor atomically $ sendNtfSubCommand ns (connId, NSCCreate) @@ -928,7 +924,7 @@ subscribeConnections' c connIds = do (subRs, rcvQs) = M.mapEither rcvQueueOrResult cs mapM_ (mapM_ (\(cData, sqs) -> mapM_ (lift . resumeMsgDelivery c cData) sqs) . sndQueue) cs mapM_ (resumeConnCmds c) $ M.keys cs - rcvRs <- lift $ connResults <$> subscribeQueues c (concat $ M.elems rcvQs) + rcvRs <- lift $ connResults . fst <$> subscribeQueues c (concat $ M.elems rcvQs) ns <- asks ntfSupervisor tkn <- readTVarIO (ntfTkn ns) when (instantNotifications tkn) . void . lift . forkIO . void . runExceptT $ sendNtfCreate ns rcvRs conns @@ -1326,13 +1322,13 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork Left e -> do let err = if msgType == AM_A_MSG_ then MERR mId e else ERR e case e of - SMP SMP.QUOTA -> case msgType of + SMP _ SMP.QUOTA -> case msgType of AM_CONN_INFO -> connError msgId NOT_AVAILABLE AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE _ -> do expireTs <- addUTCTime (-quotaExceededTimeout) <$> liftIO getCurrentTime if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RISlow - SMP SMP.AUTH -> case msgType of + SMP _ SMP.AUTH -> case msgType of AM_CONN_INFO -> connError msgId NOT_AVAILABLE AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE AM_RATCHET_INFO -> connError msgId NOT_AVAILABLE @@ -1508,10 +1504,10 @@ switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs s -- try to get the server that is different from all queues, or at least from the primary rcv queue srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs) srv' <- if srv == server then getNextServer c userId [server] else pure srvAuth - (q, qUri) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe + (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq' - liftIO $ addSubscription c rq'' + lift $ addNewQueueSubscription c rq'' tSess sessId void . enqueueMessages c cData sqs SMP.noMsgFlags $ QADD [(qUri, Just (server, sndId))] rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSendingQADD let rqs' = updatedQs rq1 rqs <> [rq''] @@ -1565,7 +1561,7 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM () ackQueueMessage c rq srvMsgId = sendAck c rq srvMsgId `catchAgentError` \case - SMP SMP.NO_MSG -> pure () + SMP _ SMP.NO_MSG -> pure () e -> throwError e -- | Suspend SMP agent connection (OFF command) in Reader monad @@ -1895,7 +1891,7 @@ deleteToken_ c tkn@NtfToken {ntfTokenId, ntfTknStatus} = do withStore' c $ \db -> updateNtfToken db tkn ntfTknStatus ntfTknAction atomically $ nsUpdateToken ns tkn {ntfTknStatus, ntfTknAction} agentNtfDeleteToken c tknId tkn `catchAgentError` \case - NTF AUTH -> pure () + NTF _ AUTH -> pure () e -> throwError e withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns @@ -1912,7 +1908,7 @@ withToken c tkn@NtfToken {deviceToken, ntfMode} from_ (toStatus, toAction_) f = let updatedToken = tkn {ntfTknStatus = toStatus, ntfTknAction = toAction_} atomically $ nsUpdateToken ns updatedToken pure toStatus - Left e@(NTF AUTH) -> do + Left e@(NTF _ AUTH) -> do withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns void $ registerNtfToken' c deviceToken ntfMode @@ -1995,11 +1991,13 @@ getSMPServer c userId = withUserServers c userId pickServer {-# INLINE getSMPServer #-} subscriber :: AgentClient -> AM' () -subscriber c@AgentClient {msgQ} = forever $ do +subscriber c@AgentClient {subQ, msgQ} = forever $ do t <- atomically $ readTBQueue msgQ agentOperationBracket c AORcvNetwork waitUntilActive $ runExceptT (processSMPTransmission c t) >>= \case - Left e -> liftIO $ print e + Left e -> do + logError $ tshow e + atomically $ writeTBQueue subQ ("", "", APC SAEConn $ ERR e) Right _ -> return () cleanupManager :: AgentClient -> AM' () @@ -2076,8 +2074,8 @@ data ACKd = ACKd | ACKPending -- | make sure to ACK or throw in each message processing branch -- it cannot be finally, unfortunately, as sometimes it needs to be ACK+DEL -processSMPTransmission :: AgentClient -> ServerTransmission SMPVersion BrokerMsg -> AM () -processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, sessId, isResponse, rId, cmd) = do +processSMPTransmission :: AgentClient -> ServerTransmission SMPVersion ErrorType BrokerMsg -> AM () +processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, sessId, tType, rId, cmd) = do (rq, SomeConn _ conn) <- withStore c (\db -> getRcvConn db srv rId) processSMP rq conn $ toConnData conn where @@ -2087,14 +2085,15 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, conn cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} = withConnLock c connId "processSMP" $ case cmd of - SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> + Right (SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId}) -> void . handleNotifyAck $ do + isGET <- atomically $ hasGetLock c rq + unless isGET checkExpiredResponse msg' <- decryptSMPMessage rq msg ack' <- handleNotifyAck $ case msg' of SMP.ClientRcvMsgBody {msgTs = srvTs, msgFlags, msgBody} -> processClientMsg srvTs msgFlags msgBody SMP.ClientRcvMsgQuota {} -> queueDrained >> ack - whenM (atomically $ hasGetLock c rq) $ - notify (MSGNTF $ SMP.rcvMessageMeta srvMsgId msg') + when isGET $ notify (MSGNTF $ SMP.rcvMessageMeta srvMsgId msg') pure ack' where queueDrained = case conn of @@ -2237,7 +2236,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, ackDel aId = enqueueCmd (ICAckDel rId srvMsgId aId) $> ACKd handleNotifyAck :: AM ACKd -> AM ACKd handleNotifyAck m = m `catchAgentError` \e -> notify (ERR e) >> ack - SMP.END -> + Right SMP.END -> atomically (TM.lookup tSess smpClients $>>= (tryReadTMVar . sessionVar) >>= processEND) >>= logServer "<--" c srv rId where @@ -2250,9 +2249,10 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, | otherwise -> ignored _ -> ignored ignored = pure "END from disconnected client - ignored" - _ -> do - logServer "<--" c srv rId $ "unexpected: " <> bshow cmd - notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED + Right (SMP.ERR e) -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e + Right SMP.OK -> checkExpiredResponse + Right _ -> unexpected + Left e -> notify $ ERR $ protocolClientError SMP (B.unpack $ strEncode srv) e where notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m () notify = atomically . notify' @@ -2266,6 +2266,27 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, enqueueCmd :: InternalCommand -> AM () enqueueCmd = enqueueCommand c "" connId (Just srv) . AInternalCommand + unexpected :: AM () + unexpected = do + logServer "<--" c srv rId $ "unexpected: " <> bshow cmd + -- TODO add extended information about transmission type once UNEXPECTED has string + notify . ERR $ BROKER (B.unpack $ strEncode srv) UNEXPECTED + + checkExpiredResponse :: AM () + checkExpiredResponse = case tType of + TTEvent -> pure () + TTUncorrelatedResponse -> unexpected + TTExpiredResponse (SMP.Cmd _ cmd') -> case cmd' of + SMP.SUB -> do + added <- + atomically $ + ifM + ((&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId) + (True <$ addSubscription c rq) + (pure False) + when added $ notify $ UP srv [connId] + _ -> pure () + decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> AM (SMP.PrivHeader, AgentMsgEnvelope) decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do clientMsg <- agentCbDecrypt e2eDh cmNonce cmEncBody diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index cfed6b932..d9b059d59 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -41,6 +41,7 @@ module Simplex.Messaging.Agent.Client getQueueMessage, decryptSMPMessage, addSubscription, + addNewQueueSubscription, getSubscriptions, sendConfirmation, sendInvitation, @@ -77,11 +78,14 @@ module Simplex.Messaging.Agent.Client logSecret, removeSubscription, hasActiveSubscription, + hasPendingSubscription, hasGetLock, + activeClientSession, agentClientStore, agentDRG, getAgentSubscriptions, slowNetworkConfig, + protocolClientError, Worker (..), SessionVar (..), SubscriptionsInfo (..), @@ -152,7 +156,7 @@ import Data.Bifunctor (bimap, first, second) import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Either (lefts, partitionEithers) +import Data.Either (partitionEithers) import Data.Functor (($>)) import Data.Int (Int64) import Data.List (deleteFirstsBy, foldl', partition, (\\)) @@ -229,7 +233,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SMPVersion) +import Simplex.Messaging.Transport (SMPVersion, SessionId, THandleParams (sessionId)) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -260,7 +264,7 @@ data AgentClient = AgentClient active :: TVar Bool, rcvQ :: TBQueue (ATransmission 'Client), subQ :: TBQueue (ATransmission 'Agent), - msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg), + msgQ :: TBQueue (ServerTransmission SMPVersion ErrorType BrokerMsg), smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), smpClients :: TMap SMPTransportSession SMPClientVar, ntfServers :: TVar [NtfServer], @@ -511,7 +515,7 @@ agentDRG AgentClient {agentEnv = Env {random}} = random class (Encoding err, Show err) => ProtocolServerClient v err msg | msg -> v, msg -> err where type Client msg = c | c -> msg getProtocolServerClient :: AgentClient -> TransportSession msg -> AM (Client msg) - clientProtocolError :: err -> AgentErrorType + clientProtocolError :: HostName -> err -> AgentErrorType closeProtocolServerClient :: Client msg -> IO () clientServer :: Client msg -> String clientTransportHost :: Client msg -> TransportHost @@ -644,7 +648,7 @@ reconnectSMPClient tc c tSess@(_, srv, _) qs = do resubscribe :: AM () resubscribe = do cs <- readTVarIO $ RQ.getConnections $ activeSubs c - rs <- lift . subscribeQueues c $ L.toList qs + (rs, sessId_) <- lift . subscribeQueues c $ L.toList qs let (errs, okConns) = partitionEithers $ map (\(RcvQueue {connId}, r) -> bimap (connId,) (const connId) r) rs liftIO $ do let conns = filter (`M.notMember` cs) okConns @@ -653,7 +657,10 @@ reconnectSMPClient tc c tSess@(_, srv, _) qs = do liftIO $ mapM_ (\(connId, e) -> notifySub connId $ ERR e) finalErrs forM_ (listToMaybe tempErrs) $ \(_, err) -> do when (null okConns && M.null cs && null finalErrs) . liftIO $ - closeClient c smpClients tSess + forM_ sessId_ $ \sessId -> do + -- We only close the client session that was used to subscribe. + v_ <- atomically $ ifM (activeClientSession c tSess sessId) (TM.lookupDelete tSess $ smpClients c) (pure Nothing) + mapM_ (closeClient_ c) v_ throwError err notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) @@ -938,13 +945,13 @@ withXFTPClient c (userId, srv, entityId) cmdStr action = do tSess <- liftIO $ mkTransportSession c userId srv entityId withLogClient c tSess entityId cmdStr action -liftClient :: (Show err, Encoding err) => (err -> AgentErrorType) -> HostName -> ExceptT (ProtocolClientError err) IO a -> AM a +liftClient :: (Show err, Encoding err) => (HostName -> err -> AgentErrorType) -> HostName -> ExceptT (ProtocolClientError err) IO a -> AM a liftClient protocolError_ = liftError . protocolClientError protocolError_ {-# INLINE liftClient #-} -protocolClientError :: (Show err, Encoding err) => (err -> AgentErrorType) -> HostName -> ProtocolClientError err -> AgentErrorType +protocolClientError :: (Show err, Encoding err) => (HostName -> err -> AgentErrorType) -> HostName -> ProtocolClientError err -> AgentErrorType protocolClientError protocolError_ host = \case - PCEProtocolError e -> protocolError_ e + PCEProtocolError e -> protocolError_ host e PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e PCEUnexpectedResponse _ -> BROKER host UNEXPECTED PCEResponseTimeout -> BROKER host TIMEOUT @@ -1023,7 +1030,7 @@ runXFTPServerTest c userId (ProtoServerWithAuth srv auth) = do liftError (testErr TSUploadFile) $ X.uploadXFTPChunk xftp spKey sId chunkSpec liftError (testErr TSDownloadFile) $ X.downloadXFTPChunk g xftp rpKey rId $ XFTPRcvChunkSpec rcvPath chSize digest rcvDigest <- liftIO $ C.sha256Hash <$> B.readFile rcvPath - unless (digest == rcvDigest) $ throwError $ ProtocolTestFailure TSCompareFile $ XFTP DIGEST + unless (digest == rcvDigest) $ throwError $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST liftError (testErr TSDeleteFile) $ X.deleteXFTPChunk xftp spKey sId ok <- tcpTimeout xftpNetworkConfig `timeout` X.closeXFTPClient xftp incClientStat c userId xftp "XFTP_TEST" "OK" @@ -1098,7 +1105,7 @@ getSessionMode :: AgentClient -> IO TransportSessionMode getSessionMode = atomically . fmap sessionMode . getNetworkConfig {-# INLINE getSessionMode #-} -newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri) +newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do C.AuthAlg a <- asks (rcvAuthAlg . config) g <- asks random @@ -1107,8 +1114,9 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do (e2eDhKey, e2ePrivKey) <- atomically $ C.generateKeyPair g logServer "-->" c srv "" "NEW" tSess <- liftIO $ mkTransportSession c userId srv connId - QIK {rcvId, sndId, rcvPublicDhKey} <- - withClient c tSess "NEW" $ \smp -> createSMPQueue smp rKeys dhKey auth subMode + (sessId, QIK {rcvId, sndId, rcvPublicDhKey}) <- + withClient c tSess "NEW" $ \smp -> + (sessionId $ thParams smp,) <$> createSMPQueue smp rKeys dhKey auth subMode liftIO . logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = RcvQueue @@ -1130,17 +1138,18 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do clientNtfCreds = Nothing, deleteErrors = 0 } - pure (rq, SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey) + qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey + pure (rq, qUri, tSess, sessId) -processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> IO (Either SMPClientError ()) -processSubResult c rq r = do - case r of - Left e -> - unless (temporaryClientError e) . atomically $ do - RQ.deleteQueue rq (pendingSubs c) - TM.insert (RQ.qKey rq) e (removedSubs c) - _ -> addSubscription c rq - pure r +processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> STM () +processSubResult c rq@RcvQueue {connId} = \case + Left e -> + unless (temporaryClientError e) $ do + RQ.deleteQueue rq (pendingSubs c) + TM.insert (RQ.qKey rq) e (removedSubs c) + Right () -> + whenM (hasPendingSubscription c connId) $ + addSubscription c rq temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case @@ -1157,7 +1166,7 @@ temporaryOrHostError = \case {-# INLINE temporaryOrHostError #-} -- | Subscribe to queues. The list of results can have a different order. -subscribeQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] +subscribeQueues :: AgentClient -> [RcvQueue] -> AM' ([(RcvQueue, Either AgentErrorType ())], Maybe SessionId) subscribeQueues c qs = do (errs, qs') <- partitionEithers <$> mapM checkQueue qs atomically $ do @@ -1165,20 +1174,43 @@ subscribeQueues c qs = do RQ.batchAddQueues (pendingSubs c) qs' env <- ask -- only "checked" queues are subscribed - (errs <>) <$> sendTSessionBatches "SUB" 90 id (subscribeQueues_ env) c qs' + session <- newTVarIO Nothing + rs <- sendTSessionBatches "SUB" 90 id (subscribeQueues_ env session) c qs' + (errs <> rs,) <$> readTVarIO session where checkQueue rq = do prohibited <- atomically $ hasGetLock c rq pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED) else Right rq - subscribeQueues_ :: Env -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) - subscribeQueues_ env smp qs' = do + subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) + subscribeQueues_ env session smp qs' = do rs <- sendBatch subscribeSMPQueues smp qs' - mapM_ (uncurry $ processSubResult c) rs - when (any temporaryClientError . lefts . map snd $ L.toList rs) $ - runReaderT (resubscribeSMPSession c $ transportSession' smp) env - pure rs + active <- + atomically $ + ifM + (activeClientSession c tSess sessId) + (writeTVar session (Just sessId) >> processSubResults rs $> True) + (pure False) + if active + then when (hasTempErrors rs) resubscribe $> rs + else do + logWarn "subcription batch result for replaced SMP client, resubscribing" + resubscribe $> L.map (second $ \_ -> Left PCENetworkError) rs + where + tSess = transportSession' smp + sessId = sessionId $ thParams smp + hasTempErrors = any (either temporaryClientError (const False) . snd) + processSubResults :: NonEmpty (RcvQueue, Either SMPClientError ()) -> STM () + processSubResults = mapM_ $ uncurry $ processSubResult c + resubscribe = resubscribeSMPSession c tSess `runReaderT` env -type BatchResponses e r = (NonEmpty (RcvQueue, Either e r)) +activeClientSession :: AgentClient -> SMPTransportSession -> SessionId -> STM Bool +activeClientSession c tSess sessId = sameSess <$> tryReadSessVar tSess (smpClients c) + where + sameSess = \case + Just (Right smp) -> sessId == sessionId (thParams smp) + _ -> False + +type BatchResponses e r = NonEmpty (RcvQueue, Either e r) -- statBatchSize is not used to batch the commands, only for traffic statistics sendTSessionBatches :: forall q r. ByteString -> Int -> (q -> RcvQueue) -> (SMPClient -> NonEmpty q -> IO (BatchResponses SMPClientError r)) -> AgentClient -> [q] -> AM' [(RcvQueue, Either AgentErrorType r)] @@ -1213,16 +1245,35 @@ sendBatch smpCmdFunc smp qs = L.zip qs <$> smpCmdFunc smp (L.map queueCreds qs) where queueCreds RcvQueue {rcvPrivateKey, rcvId} = (rcvPrivateKey, rcvId) -addSubscription :: AgentClient -> RcvQueue -> IO () -addSubscription c rq@RcvQueue {connId} = atomically $ do +addSubscription :: AgentClient -> RcvQueue -> STM () +addSubscription c rq@RcvQueue {connId} = do modifyTVar' (subscrConns c) $ S.insert connId RQ.addQueue rq $ activeSubs c RQ.deleteQueue rq $ pendingSubs c +addPendingSubscription :: AgentClient -> RcvQueue -> STM () +addPendingSubscription c rq@RcvQueue {connId} = do + modifyTVar' (subscrConns c) $ S.insert connId + RQ.addQueue rq $ pendingSubs c + +addNewQueueSubscription :: AgentClient -> RcvQueue -> SMPTransportSession -> SessionId -> AM' () +addNewQueueSubscription c rq tSess sessId = do + same <- + atomically $ + ifM + (activeClientSession c tSess sessId) + (True <$ addSubscription c rq) + (False <$ addPendingSubscription c rq) + unless same $ resubscribeSMPSession c tSess + hasActiveSubscription :: AgentClient -> ConnId -> STM Bool hasActiveSubscription c connId = RQ.hasConn connId $ activeSubs c {-# INLINE hasActiveSubscription #-} +hasPendingSubscription :: AgentClient -> ConnId -> STM Bool +hasPendingSubscription c connId = RQ.hasConn connId $ pendingSubs c +{-# INLINE hasPendingSubscription #-} + removeSubscription :: AgentClient -> ConnId -> STM () removeSubscription c connId = do modifyTVar' (subscrConns c) $ S.delete connId diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 98db26ab4..e136a8bbb 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -193,13 +193,13 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), PQEncryption (..), - pattern PQEncOff, PQSupport, - pattern PQSupportOn, - pattern PQSupportOff, RcvE2ERatchetParams, RcvE2ERatchetParamsUri, - SndE2ERatchetParams + SndE2ERatchetParams, + pattern PQEncOff, + pattern PQSupportOff, + pattern PQSupportOn, ) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -213,14 +213,14 @@ import Simplex.Messaging.Protocol MsgId, NMsgMeta, ProtocolServer (..), + SMPClientVersion, SMPMsgMeta, SMPServer, SMPServerWithAuth, SndPublicAuthKey, SubscriptionMode, - SMPClientVersion, - VersionSMPC, VersionRangeSMPC, + VersionSMPC, initialSMPClientVersion, legacyEncodeServer, legacyServerP, @@ -908,7 +908,7 @@ instance Encoding AgentMsgEnvelope where -- AgentRatchetInfo is not encrypted with double ratchet, but with per-queue E2E encryption data AgentMessage = -- used by the initiating party when confirming reply queue - AgentConnInfo ConnInfo + AgentConnInfo ConnInfo | -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation. -- It made removed REPLY message unnecessary. AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo @@ -1382,9 +1382,9 @@ deriving instance Show (ConnectionRequestUri m) data AConnectionRequestUri = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequestUri m) instance Eq AConnectionRequestUri where - ACR m cr == ACR m' cr' = case testEquality m m' of - Just Refl -> cr == cr' - _ -> False + ACR m cr == ACR m' cr' = case testEquality m m' of + Just Refl -> cr == cr' + _ -> False deriving instance Show AConnectionRequestUri @@ -1469,11 +1469,11 @@ data AgentErrorType | -- | connection errors CONN {connErr :: ConnectionErrorType} | -- | SMP protocol errors forwarded to agent clients - SMP {smpErr :: ErrorType} + SMP {serverAddress :: String, smpErr :: ErrorType} | -- | NTF protocol errors forwarded to agent clients - NTF {ntfErr :: ErrorType} + NTF {serverAddress :: String, ntfErr :: ErrorType} | -- | XFTP protocol errors forwarded to agent clients - XFTP {xftpErr :: XFTPErrorType} + XFTP {serverAddress :: String, xftpErr :: XFTPErrorType} | -- | XRCP protocol errors forwarded to agent clients RCP {rcpErr :: RCErrorType} | -- | SMP server errors @@ -1584,9 +1584,9 @@ instance StrEncoding AgentErrorType where strP = "CMD " *> (CMD <$> parseRead1) <|> "CONN " *> (CONN <$> parseRead1) - <|> "SMP " *> (SMP <$> strP) - <|> "NTF " *> (NTF <$> strP) - <|> "XFTP " *> (XFTP <$> strP) + <|> "SMP " *> (SMP <$> textP <*> _strP) + <|> "NTF " *> (NTF <$> textP <*> _strP) + <|> "XFTP " *> (XFTP <$> textP <*> _strP) <|> "RCP " *> (RCP <$> strP) <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) @@ -1602,9 +1602,9 @@ instance StrEncoding AgentErrorType where strEncode = \case CMD e -> "CMD " <> bshow e CONN e -> "CONN " <> bshow e - SMP e -> "SMP " <> strEncode e - NTF e -> "NTF " <> strEncode e - XFTP e -> "XFTP " <> strEncode e + SMP srv e -> "SMP " <> text srv <> " " <> strEncode e + NTF srv e -> "NTF " <> text srv <> " " <> strEncode e + XFTP srv e -> "XFTP " <> text srv <> " " <> strEncode e RCP e -> "RCP " <> strEncode e BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 176602f4b..6cee90839 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -70,6 +70,7 @@ module Simplex.Messaging.Client proxyUsername, temporaryClientError, ServerTransmission, + TransmissionType (..), ClientCommand, -- * For testing @@ -80,9 +81,11 @@ module Simplex.Messaging.Client ) where +import Control.Applicative ((<|>)) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class (liftIO) @@ -110,7 +113,7 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient) import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Transport.WebSockets (WS) -import Simplex.Messaging.Util (bshow, diffToMicroseconds, raceAny_, threadDelay', whenM) +import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, raceAny_, threadDelay', tshow, whenM) import Simplex.Messaging.Version import System.Timeout (timeout) @@ -129,15 +132,14 @@ data PClient v err msg = PClient transportSession :: TransportSession msg, transportHost :: TransportHost, tcpTimeout :: Int, - rcvConcurrency :: Int, sendPings :: TVar Bool, lastReceived :: TVar UTCTime, timeoutErrorCount :: TVar Int, clientCorrId :: TVar ChaChaDRG, sentCommands :: TMap CorrId (Request err msg), - sndQ :: TBQueue (TVar Bool, ByteString), + sndQ :: TBQueue ByteString, rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)), - msgQ :: Maybe (TBQueue (ServerTransmission v msg)) + msgQ :: Maybe (TBQueue (ServerTransmission v err msg)) } smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient @@ -170,7 +172,6 @@ smpClientStub g sessionId thVersion thAuth = do transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing), transportHost = "localhost", tcpTimeout = 15_000_000, - rcvConcurrency = 8, sendPings, lastReceived, timeoutErrorCount, @@ -188,7 +189,9 @@ type SMPClient = ProtocolClient SMPVersion ErrorType BrokerMsg type ClientCommand msg = (Maybe C.APrivateAuthKey, EntityId, ProtoCommand msg) -- | Type synonym for transmission from some SPM server queue. -type ServerTransmission v msg = (TransportSession msg, Version v, SessionId, Bool, EntityId, msg) +type ServerTransmission v err msg = (TransportSession msg, Version v, SessionId, TransmissionType msg, EntityId, Either (ProtocolClientError err) msg) + +data TransmissionType msg = TTEvent | TTUncorrelatedResponse | TTExpiredResponse (ProtoCommand msg) data HostMode = -- | prefer (or require) onion hosts when connecting via SOCKS proxy @@ -287,6 +290,8 @@ defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) suppo data Request err msg = Request { corrId :: CorrId, entityId :: EntityId, + command :: ProtoCommand msg, + pending :: TVar Bool, responseVar :: TMVar (Either (ProtocolClientError err) msg) } @@ -333,7 +338,7 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId) -- -- A single queue can be used for multiple 'SMPClient' instances, -- as 'SMPServerTransmission' includes server information. -getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmission v msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) +getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmission v err msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do case chooseTransportHost networkConfig (host srv) of Right useHost -> @@ -341,7 +346,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize `catch` \(e :: IOException) -> pure . Left $ PCEIOError e Left e -> pure $ Left e where - NetworkConfig {tcpConnectTimeout, tcpTimeout, rcvConcurrency, smpPingInterval} = networkConfig + NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig mkProtocolClient :: TransportHost -> UTCTime -> STM (PClient v err msg) mkProtocolClient transportHost ts = do connected <- newTVar False @@ -363,7 +368,6 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize timeoutErrorCount, clientCorrId, sentCommands, - rcvConcurrency, sndQ, rcvQ, msgQ @@ -402,11 +406,11 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize atomically $ do writeTVar (connected c) True putTMVar cVar $ Right c' - raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0]) + raceAny_ ([send c' th, process c', receive c' th] <> [monitor c' | smpPingInterval > 0]) `finally` disconnected c' send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () - send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= \(active, s) -> whenM (readTVarIO active) (void $ tPutLog h s) + send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= void . tPutLog h receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () receive ProtocolClient {client_ = PClient {rcvQ, lastReceived, timeoutErrorCount}} h = forever $ do @@ -414,8 +418,8 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize getCurrentTime >>= atomically . writeTVar lastReceived atomically $ writeTVar timeoutErrorCount 0 - ping :: ProtocolClient v err msg -> IO () - ping c@ProtocolClient {client_ = PClient {sendPings, lastReceived, timeoutErrorCount}} = loop smpPingInterval + monitor :: ProtocolClient v err msg -> IO () + monitor c@ProtocolClient {client_ = PClient {sendPings, lastReceived, timeoutErrorCount}} = loop smpPingInterval where loop :: Int64 -> IO () loop delay = do @@ -439,27 +443,34 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO () processMsg c@ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr)) - | isResponse = + | not $ B.null $ bs corrId = atomically (TM.lookup corrId sentCommands) >>= \case - Nothing -> sendMsg respOrErr - Just Request {entityId, responseVar} -> atomically $ do - TM.delete corrId sentCommands - putTMVar responseVar $ response entityId - | otherwise = sendMsg respOrErr + Nothing -> sendMsg TTUncorrelatedResponse + Just Request {entityId, command, pending, responseVar} -> do + wasPending <- + atomically $ do + TM.delete corrId sentCommands + ifM + (swapTVar pending False) + (True <$ tryPutTMVar responseVar (response entityId)) + (pure False) + unless wasPending $ sendMsg $ if entityId == entId then TTExpiredResponse command else TTUncorrelatedResponse + | otherwise = sendMsg TTEvent where - isResponse = not $ B.null $ bs corrId response entityId - | entityId == entId = - case respOrErr of - Left e -> Left $ PCEResponseError e - Right r -> case protocolError r of - Just e -> Left $ PCEProtocolError e - _ -> Right r + | entityId == entId = clientResp | otherwise = Left . PCEUnexpectedResponse $ bshow respOrErr - sendMsg :: Either err msg -> IO () - sendMsg = \case - Right msg -> atomically $ mapM_ (`writeTBQueue` serverTransmission c isResponse entId msg) msgQ - Left e -> putStrLn $ "SMP client error: " <> show e + clientResp = case respOrErr of + Left e -> Left $ PCEResponseError e + Right r -> case protocolError r of + Just e -> Left $ PCEProtocolError e + _ -> Right r + sendMsg :: TransmissionType msg -> IO () + sendMsg tType = case msgQ of + Just q -> atomically $ writeTBQueue q $ serverTransmission c tType entId clientResp + Nothing -> case clientResp of + Left e -> logError $ "SMP client error: " <> tshow e + Right _ -> logWarn $ "SMP client unprocessed event" proxyUsername :: TransportSession msg -> ByteString proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_ @@ -558,11 +569,11 @@ processSUBResponse c (Response rId r) = case r of Left e -> pure $ Left e writeSMPMessage :: SMPClient -> RecipientId -> BrokerMsg -> IO () -writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c False rId msg) (msgQ $ client_ c) +writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c TTEvent rId (Right msg)) (msgQ $ client_ c) -serverTransmission :: ProtocolClient v err msg -> Bool -> RecipientId -> msg -> ServerTransmission v msg -serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} isResponse entityId message = - (transportSession, thVersion, sessionId, isResponse, entityId, message) +serverTransmission :: ProtocolClient v err msg -> TransmissionType msg -> RecipientId -> Either (ProtocolClientError err) msg -> ServerTransmission v err msg +serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} tType entityId msgOrErr = + (transportSession, thVersion, sessionId, tType, entityId, msgOrErr) -- | Get message from SMP queue. The server returns ERR PROHIBITED if a client uses SUB and GET via the same transport connection for the same queue -- @@ -687,7 +698,7 @@ sendSMPCommand c pKey qId cmd = sendProtocolCommand c pKey qId (Cmd sParty cmd) type PCTransmission err msg = (Either TransportError SentRawTransmission, Request err msg) -- | Send multiple commands with batching and collect responses -sendProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg)) +sendProtocolCommands :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg)) sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs = do bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs validate . concat =<< mapM (sendBatch c) bs @@ -704,30 +715,28 @@ sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSiz where diff = L.length cs - length rs -streamProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO () +streamProtocolCommands :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO () streamProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs cb = do bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs mapM_ (cb <=< sendBatch c) bs sendBatch :: ProtocolClient v err msg -> TransportBatch (Request err msg) -> IO [Response err msg] -sendBatch c@ProtocolClient {client_ = PClient {rcvConcurrency, sndQ}} b = do +sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do case b of TBError e Request {entityId} -> do putStrLn "send error: large message" pure [Response entityId $ Left $ PCETransportError e] TBTransmissions s n rs | n > 0 -> do - active <- newTVarIO True - atomically $ writeTBQueue sndQ (active, s) - mapConcurrently (getResponse c active) rs + atomically $ writeTBQueue sndQ s + mapConcurrently (getResponse c) rs | otherwise -> pure [] TBTransmission s r -> do - active <- newTVarIO True - atomically $ writeTBQueue sndQ (active, s) - (: []) <$> getResponse c active r + atomically $ writeTBQueue sndQ s + (: []) <$> getResponse c r -- | Send Protocol command -sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} pKey entId cmd = ExceptT $ uncurry sendRecv =<< mkTransmission c (pKey, entId, cmd) where @@ -738,30 +747,30 @@ sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THand Right t | B.length s > blockSize - 2 -> pure . Left $ PCETransportError TELargeMsg | otherwise -> do - active <- newTVarIO True - atomically (writeTBQueue sndQ (active, s)) - response <$> getResponse c active r + atomically $ writeTBQueue sndQ s + response <$> getResponse c r where s | batch = tEncodeBatch1 t | otherwise = tEncode t --- TODO switch to timeout or TimeManager that supports Int64 -getResponse :: ProtocolClient v err msg -> TVar Bool -> Request err msg -> IO (Response err msg) -getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount, sentCommands}} active Request {corrId, entityId, responseVar} = do - response <- - timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case - Just r -> atomically (writeTVar timeoutErrorCount 0) $> r - Nothing -> do - atomically (writeTVar active False >> TM.delete corrId sentCommands) - atomically $ modifyTVar' timeoutErrorCount (+ 1) - pure $ Left PCEResponseTimeout +getResponse :: ProtocolClient v err msg -> Request err msg -> IO (Response err msg) +getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount}} Request {entityId, pending, responseVar} = do + r <- tcpTimeout `timeout` atomically (takeTMVar responseVar) + response <- atomically $ do + writeTVar pending False + -- Try to read response again in case it arrived after timeout expired + -- but before `pending` was set to False above. + -- See `processMsg`. + ((r <|>) <$> tryTakeTMVar responseVar) >>= \case + Just r' -> writeTVar timeoutErrorCount 0 $> r' + Nothing -> modifyTVar' timeoutErrorCount (+ 1) $> Left PCEResponseTimeout pure Response {entityId, response} -mkTransmission :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) -mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} (pKey_, entId, cmd) = do +mkTransmission :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} (pKey_, entityId, command) = do corrId <- atomically getNextCorrId - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, entId, cmd) + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, entityId, command) auth = authTransmission (thAuth thParams) pKey_ corrId tForAuth r <- atomically $ mkRequest corrId pure ((,tToSend) <$> auth, r) @@ -770,7 +779,16 @@ mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCo getNextCorrId = CorrId <$> C.randomBytes 24 clientCorrId -- also used as nonce mkRequest :: CorrId -> STM (Request err msg) mkRequest corrId = do - r <- Request corrId entId <$> newEmptyTMVar + pending <- newTVar True + responseVar <- newEmptyTMVar + let r = + Request + { corrId, + entityId, + command, + pending, + responseVar + } TM.insert corrId r sentCommands pure r diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 4b925c6f6..aed56f1bd 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -39,7 +39,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (BrokerMsg, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer) +import Simplex.Messaging.Protocol (BrokerMsg, ErrorType, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer) import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM @@ -94,7 +94,7 @@ defaultSMPClientAgentConfig = data SMPClientAgent = SMPClientAgent { agentCfg :: SMPClientAgentConfig, - msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg), + msgQ :: TBQueue (ServerTransmission SMPVersion ErrorType BrokerMsg), agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, smpClients :: TMap SMPServer SMPClientVar, diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index b79665c87..37eff1e94 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -218,10 +218,10 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge receiveSMP :: M () receiveSMP = forever $ do - ((_, srv, _), _, _, _, ntfId, msg) <- atomically $ readTBQueue msgQ + ((_, srv, _), _, _, _tType, ntfId, msgOrErr) <- atomically $ readTBQueue msgQ let smpQueue = SMPQueueNtf srv ntfId - case msg of - SMP.NMSG nmsgNonce encNMsgMeta -> do + case msgOrErr of + Right (SMP.NMSG nmsgNonce encNMsgMeta) -> do ntfTs <- liftIO getSystemTime st <- asks store NtfPushServer {pushQ} <- asks pushServer @@ -231,8 +231,10 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge findNtfSubscriptionToken st smpQueue >>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta})) incNtfStat ntfReceived - SMP.END -> updateSubStatus smpQueue NSEnd - _ -> pure () + Right SMP.END -> updateSubStatus smpQueue NSEnd + Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e + Right _ -> logError $ "SMP server unexpected response" + Left e -> logError $ "SMP client error: " <> tshow e receiveAgent = forever $ diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 85beccd6b..a09759814 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -894,9 +894,10 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Just msg -> let encMsg = encryptMsg qr msg in atomically (setDelivered s msg) $> (corrId, rId, MSG encMsg) - _ -> forkSub $> ok - _ -> pure ok + _ -> forkSub $> resp + _ -> pure resp where + resp = (corrId, rId, OK) forkSub :: M () forkSub = do atomically . modifyTVar' sub $ \s -> s {subThread = SubPending} diff --git a/src/Simplex/Messaging/Session.hs b/src/Simplex/Messaging/Session.hs index 7a219e106..75543b481 100644 --- a/src/Simplex/Messaging/Session.hs +++ b/src/Simplex/Messaging/Session.hs @@ -10,6 +10,7 @@ import Data.Composition ((.:.)) import Data.Functor (($>)) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Util (($>>=)) data SessionVar a = SessionVar { sessionVar :: TMVar a, @@ -36,3 +37,6 @@ removeSessVar' v sessKey vs = TM.lookup sessKey vs >>= \case Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True _ -> pure False + +tryReadSessVar :: Ord k => k -> TMap k (SessionVar a) -> STM (Maybe a) +tryReadSessVar sessKey vs = TM.lookup sessKey vs $>>= (tryReadTMVar . sessionVar) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 8c06c0d82..e1d383b5a 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -127,7 +127,7 @@ smpBlockSize = 16384 -- 4 - support command batching (7/17/2022) -- 5 - basic auth for SMP servers (11/12/2022) -- 6 - allow creating queues without subscribing (9/10/2023) --- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (2/3/2024) +-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (4/30/2024) data SMPVersion diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 32610b54e..df117c105 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -242,7 +242,7 @@ testDuplexConnection' (alice, aPQ) (bob, bPQ) = do alice #: ("4a", "bob", "ACK 7") #> ("4a", "bob", OK) alice #: ("5", "bob", "OFF") #> ("5", "bob", OK) bob #: ("17", "alice", "SEND F 9\nmessage 3") #> ("17", "alice", A.MID 8 pq) - bob <# ("", "alice", MERR 8 (SMP AUTH)) + bob <#= \case ("", "alice", MERR 8 (SMP _ AUTH)) -> True; _ -> False alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #:# "nothing else should be delivered to alice" @@ -280,7 +280,7 @@ testDuplexConnRandomIds' (alice, aPQ) (bob, bPQ) = do alice #: ("4a", bobConn, "ACK 7") #> ("4a", bobConn, OK) alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK) bob #: ("17", aliceConn, "SEND F 9\nmessage 3") #> ("17", aliceConn, A.MID 8 pq) - bob <# ("", aliceConn, MERR 8 (SMP AUTH)) + bob <#= \case ("", cId, MERR 8 (SMP _ AUTH)) -> cId == aliceConn; _ -> False alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) alice #:# "nothing else should be delivered to alice" @@ -383,7 +383,7 @@ testSubscrNotification t (server, _) client = do killThread server client <#. ("", "", DOWN testSMPServer ["conn1"]) withSmpServer (ATransport t) $ - client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue + client <#= \case ("", "conn1", ERR (SMP _ AUTH)) -> True; _ -> False -- this new server does not have the queue testMsgDeliveryServerRestart :: forall c. Transport c => (c, InitialKeys) -> (c, PQSupport) -> IO () testMsgDeliveryServerRestart (alice, aPQ) (bob, bPQ) = do @@ -630,7 +630,7 @@ syntaxTests t = do <> " subscribe " <> "14\nbob's connInfo" ) - >#> ("311", "a", "ERR SMP AUTH") + >#> ("311", "a", "ERR SMP smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001 AUTH") describe "invalid" $ do it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX") where diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 59e433ea5..f1dcc058a 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -422,7 +422,7 @@ functionalAPITests t = do describe "server with password" $ do let auth = Just "abcd" srv = ProtoServerWithAuth testSMPServer2 - authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP AUTH) + authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP (B.unpack $ strEncode testSMPServer2) AUTH) it "should pass with correct password" $ testSMPServerConnectionTest t auth (srv auth) `shouldReturn` Nothing it "should fail without password" $ testSMPServerConnectionTest t auth (srv Nothing) `shouldReturn` authErr it "should fail with incorrect password" $ testSMPServerConnectionTest t auth (srv $ Just "wrong") `shouldReturn` authErr @@ -537,7 +537,7 @@ runAgentClientTest pqSupport alice@AgentClient {} bob baseId = ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId 5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where @@ -669,7 +669,7 @@ runAgentClientContactTest pqSupport alice bob baseId = ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId 5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where @@ -1115,7 +1115,7 @@ testExpireMessageQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testP 5 <- sendMessage a bId SMP.noMsgFlags "2" liftIO $ threadDelay 1000000 6 <- sendMessage a bId SMP.noMsgFlags "3" -- this won't expire - get a =##> \case ("", c, MERR 5 (SMP QUOTA)) -> bId == c; _ -> False + get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False pure (aId, bId) withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do subscribeConnection b' aId @@ -1143,15 +1143,15 @@ testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} 7 <- sendMessage a bId SMP.noMsgFlags "4" liftIO $ threadDelay 1000000 8 <- sendMessage a bId SMP.noMsgFlags "5" -- this won't expire - get a =##> \case ("", c, MERR 5 (SMP QUOTA)) -> bId == c; _ -> False + get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False get a >>= \case - ("", c, MERR 6 (SMP QUOTA)) -> do + ("", c, MERR 6 (SMP _ QUOTA)) -> do liftIO $ bId `shouldBe` c - get a =##> \case ("", c', MERR 7 (SMP QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP QUOTA)) -> bId == c'; _ -> False - ("", c, MERRS [6] (SMP QUOTA)) -> do + get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP _ QUOTA)) -> bId == c'; _ -> False + ("", c, MERRS [6] (SMP _ QUOTA)) -> do liftIO $ bId `shouldBe` c - get a =##> \case ("", c', MERR 7 (SMP QUOTA)) -> bId == c'; _ -> False - ("", c, MERRS [6, 7] (SMP QUOTA)) -> liftIO $ bId `shouldBe` c + get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; _ -> False + ("", c, MERRS [6, 7] (SMP _ QUOTA)) -> liftIO $ bId `shouldBe` c r -> error $ show r pure (aId, bId) withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do @@ -1402,10 +1402,10 @@ makeConnection = makeConnection_ PQSupportOn makeConnection_ :: PQSupport -> AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnection_ pqEnc alice bob = makeConnectionForUsers_ pqEnc alice 1 bob 1 -makeConnectionForUsers :: AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) +makeConnectionForUsers :: HasCallStack => AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn -makeConnectionForUsers_ :: PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) +makeConnectionForUsers_ :: HasCallStack => PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers_ pqSupport alice aliceUserId bob bobUserId = do (bobId, qInfo) <- A.createConnection alice aliceUserId True SCMInvitation Nothing (CR.IKNoPQ pqSupport) SMSubscribe aliceId <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport @@ -1709,7 +1709,7 @@ testAcceptContactAsync = ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId 5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where @@ -1755,7 +1755,7 @@ testWaitDeliveryNoPending t = withAgentClients2 $ \alice bob -> get alice =##> \case ("", cId, DEL_CONN) -> cId == bobId; _ -> False 3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 3) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 3); _ -> False liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" @@ -1850,8 +1850,8 @@ testWaitDeliveryAUTHErr t = liftIO $ noMessages bob "nothing else should be delivered to bob" withSmpServerStoreLogOn t testPort $ \_ -> do - get alice ##> ("", bobId, MERR (baseId + 3) (SMP AUTH)) - get alice ##> ("", bobId, MERR (baseId + 4) (SMP AUTH)) + get alice =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == bobId && mId == (baseId + 3); _ -> False + get alice =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == bobId && mId == (baseId + 4); _ -> False get alice =##> \case ("", cId, DEL_CONN) -> cId == bobId; _ -> False liftIO $ noMessages alice "nothing else should be delivered to alice" @@ -2422,11 +2422,11 @@ testCreateQueueAuth srvVersion clnt1 clnt2 = do b <- getClient 2 clnt2 testDB2 r <- runRight $ do tryError (createConnection a 1 True SCMInvitation Nothing SMSubscribe) >>= \case - Left (SMP AUTH) -> pure 0 + Left (SMP _ AUTH) -> pure 0 Left e -> throwError e Right (bId, qInfo) -> tryError (joinConnection b 1 True qInfo "bob's connInfo" SMSubscribe) >>= \case - Left (SMP AUTH) -> pure 1 + Left (SMP _ AUTH) -> pure 1 Left e -> throwError e Right aId -> do ("", _, CONF confId _ "bob's connInfo") <- get a diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 2c1045791..3b497c8d4 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.NotificationTests where @@ -17,10 +17,6 @@ import AgentTests.FunctionalAPITests createConnection, exchangeGreetingsMsgId, get, - withAgent, - withAgentClients2, - withAgentClientsCfgServers2, - withAgentClients3, joinConnection, makeConnection, nGet, @@ -29,7 +25,11 @@ import AgentTests.FunctionalAPITests sendMessage, switchComplete, testServerMatrix2, + withAgent, + withAgentClients2, + withAgentClients3, withAgentClientsCfg2, + withAgentClientsCfgServers2, (##>), (=##>), pattern CON, @@ -59,8 +59,8 @@ import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO) import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Protocol +import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Types (NtfToken (..)) import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) @@ -151,7 +151,8 @@ testNtfMatrix t runTest = do it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest skip "this case cannot be supported - see RFC" $ - it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest + it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ + runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest -- servers can be migrated in any order it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest @@ -243,7 +244,7 @@ testNtfTokenSecondRegistration APNSMockServer {apnsQ} = -- now the second token registration is verified verifyNtfToken a' tkn nonce' verification' -- the first registration is removed - Left (NTF AUTH) <- tryE $ checkNtfToken a tkn + Left (NTF _ AUTH) <- tryE $ checkNtfToken a tkn -- and the second is active NTActive <- checkNtfToken a' tkn pure () @@ -258,7 +259,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do atomically $ readTBQueue apnsQ liftIO $ sendApnsResponse APNSRespOk pure ntfData - -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server + -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server threadDelay 1000000 withAgent 2 agentCfg initAgentServers testDB $ \a' -> -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, @@ -266,7 +267,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do withNtfServer t . runRight_ $ do verification <- ntfData .-> "verification" nonce <- C.cbNonce <$> ntfData .-> "nonce" - Left (NTF AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification + Left (NTF _ AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <- atomically $ readTBQueue apnsQ verification' <- ntfData' .-> "verification" diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 7b1a7b813..d574bfb4f 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -35,6 +35,9 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do errHasSpaces = \case BROKER srv (RESPONSE e) -> hasSpaces srv || hasSpaces e BROKER srv _ -> hasSpaces srv + SMP srv _ -> hasSpaces srv + NTF srv _ -> hasSpaces srv + XFTP srv _ -> hasSpaces srv _ -> False hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 88786bb40..0610bf48d 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -69,7 +69,7 @@ xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do describe "server with password" $ do let auth = Just "abcd" srv = ProtoServerWithAuth testXFTPServer2 - authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP AUTH) + authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP (B.unpack $ strEncode testXFTPServer2) AUTH) it "should pass with correct password" $ testXFTPServerTest auth (srv auth) `shouldReturn` Nothing it "should fail without password" $ testXFTPServerTest auth (srv Nothing) `shouldReturn` authErr it "should fail with incorrect password" $ testXFTPServerTest auth (srv $ Just "wrong") `shouldReturn` authErr @@ -392,7 +392,8 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do -- receive file - should fail with AUTH error withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do runRight_ $ xftpStartWorkers rcp' (Just recipientFiles) - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp' + ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfGet rcp' rfId' `shouldBe` rfId -- tmp path should be removed after permanent error @@ -471,7 +472,8 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do -- send file - should fail with AUTH error withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do runRight_ $ xftpStartWorkers sndr' (Just senderFiles) - ("", sfId', SFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- sfGet sndr' + ("", sfId', SFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + sfGet sndr' sfId' `shouldBe` sfId -- prefix path should be removed after permanent error @@ -506,7 +508,8 @@ testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $ withAgent 3 agentCfg initAgentServers testDB2 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2 + ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfGet rcp2 liftIO $ rfId' `shouldBe` rfId testXFTPAgentDeleteRestore :: HasCallStack => IO () @@ -543,7 +546,8 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do withAgent 5 agentCfg initAgentServers testDB3 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2 + ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfGet rcp2 liftIO $ rfId' `shouldBe` rfId testXFTPAgentDeleteOnServer :: HasCallStack => IO () @@ -577,7 +581,8 @@ testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $ runRight_ . void $ do -- receive file 1 again rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing - ("", rfId1', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp + ("", rfId1', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfGet rcp liftIO $ rfId1 `shouldBe` rfId1' -- receive file 2 @@ -609,7 +614,8 @@ testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do -- receive file 1 again - should fail with AUTH error runRight $ do rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp + ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfGet rcp liftIO $ rfId' `shouldBe` rfId -- create and send file 2 From 66c916dbb33e625cc40cc43aa2aededab859156b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 12 May 2024 21:12:01 +0100 Subject: [PATCH 034/125] proxy: increase client timeout for proxied commands (#1145) --- src/Simplex/Messaging/Client.hs | 35 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 92dd4047b..62d75c3e2 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -143,6 +143,7 @@ data PClient v err msg = PClient { connected :: TVar Bool, transportSession :: TransportSession msg, transportHost :: TransportHost, + tcpConnectTimeout :: Int, tcpTimeout :: Int, rcvConcurrency :: Int, sendPings :: TVar Bool, @@ -185,6 +186,7 @@ smpClientStub g sessionId thVersion thAuth = do { connected, transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing), transportHost = "localhost", + tcpConnectTimeout = 20_000_000, tcpTimeout = 15_000_000, rcvConcurrency = 8, sendPings, @@ -413,6 +415,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize { connected, transportSession, transportHost, + tcpConnectTimeout, tcpTimeout, sendPings, lastReceived, @@ -735,9 +738,9 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay -connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth +connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth | thVersion (thParams c) >= sendingProxySMPVersion = - sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case + sendProtocolCommand_ c Nothing tOut Nothing "" (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case PKEY sId vr (chain, key) -> case supportedClientSMPRelayVRange `compatibleVersion` vr of Nothing -> throwE $ transportErr TEVersion @@ -745,6 +748,7 @@ connectSMPProxiedRelay c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxy r -> throwE . PCEUnexpectedResponse $ bshow r | otherwise = throwE $ PCETransportError TEVersion where + tOut = Just $ tcpConnectTimeout + tcpTimeout transportErr = PCEProtocolError . PROXY . BROKER . TRANSPORT validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 validateRelay (X.CertificateChain cert) exact = do @@ -819,7 +823,7 @@ proxySMPMessage :: MsgFlags -> MsgBody -> ExceptT SMPClientError IO (Either ProxyClientError ()) -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} @@ -827,7 +831,7 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c let cmdSecret = C.dh' serverKey cmdPrivKey nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g -- encode - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg) + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender (SEND flags msg)) auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of [] -> throwE $ PCETransportError TELargeMsg @@ -836,7 +840,8 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c TBTransmissions s _ _ : _ -> pure s et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength -- proxy interaction errors are wrapped - tryE (sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case + let tOut = Just $ 2 * tcpTimeout + tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case Right r -> case r of PRES (EncResponse er) -> do -- server interaction errors are thrown directly @@ -872,7 +877,7 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission} eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send - sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case + sendProtocolCommand_ c (Just nonce) Nothing Nothing "" (Cmd SSender (RFWD eft)) >>= \case RRES (EncFwdResponse efr) -> do -- unwrap r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr @@ -936,19 +941,19 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do | n > 0 -> do active <- newTVarIO True atomically $ writeTBQueue sndQ (active, s) - mapConcurrently (getResponse c active) rs + mapConcurrently (getResponse c Nothing active) rs | otherwise -> pure [] TBTransmission s r -> do active <- newTVarIO True atomically $ writeTBQueue sndQ (active, s) - (: []) <$> getResponse c active r + (: []) <$> getResponse c Nothing active r -- | Send Protocol command sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand c = sendProtocolCommand_ c Nothing +sendProtocolCommand c = sendProtocolCommand_ c Nothing Nothing -sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ pKey entId cmd = +sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe Int -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ tOut pKey entId cmd = ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd) where -- two separate "atomically" needed to avoid blocking @@ -960,17 +965,17 @@ sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THan | otherwise -> do active <- newTVarIO True atomically (writeTBQueue sndQ (active, s)) - response <$> getResponse c active r + response <$> getResponse c tOut active r where s | batch = tEncodeBatch1 t | otherwise = tEncode t -- TODO switch to timeout or TimeManager that supports Int64 -getResponse :: ProtocolClient v err msg -> TVar Bool -> Request err msg -> IO (Response err msg) -getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount, sentCommands}} active Request {corrId, entityId, responseVar} = do +getResponse :: ProtocolClient v err msg -> Maybe Int -> TVar Bool -> Request err msg -> IO (Response err msg) +getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount, sentCommands}} tOut active Request {corrId, entityId, responseVar} = do response <- - timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case + fromMaybe tcpTimeout tOut `timeout` atomically (takeTMVar responseVar) >>= \case Just r -> atomically (writeTVar timeoutErrorCount 0) $> r Nothing -> do atomically (writeTVar active False >> TM.delete corrId sentCommands) From 4455b8bd0e243aa3bb4dc854037b2e64677963b0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 13 May 2024 08:10:40 +0100 Subject: [PATCH 035/125] agent: do not throw exception when command is created for deleted connection (#1150) * agent: do not throw exception when command is created for deleted connection * convert database busy/locked to critical alert --- src/Simplex/Messaging/Agent.hs | 2 +- src/Simplex/Messaging/Agent/Client.hs | 14 +++++++++++--- src/Simplex/Messaging/Agent/Store.hs | 4 +++- src/Simplex/Messaging/Agent/Store/SQLite.hs | 13 +++++++++---- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index f834927d8..198fabdba 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1994,7 +1994,7 @@ subscriber :: AgentClient -> AM' () subscriber c@AgentClient {subQ, msgQ} = forever $ do t <- atomically $ readTBQueue msgQ agentOperationBracket c AORcvNetwork waitUntilActive $ - runExceptT (processSMPTransmission c t) >>= \case + tryAgentError' (processSMPTransmission c t) >>= \case Left e -> do logError $ tshow e atomically $ writeTBQueue subQ ("", "", APC SAEConn $ ERR e) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index d9b059d59..4c359a519 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -172,6 +172,7 @@ import Data.Text.Encoding import Data.Time (UTCTime, defaultTimeLocale, diffUTCTime, formatTime, getCurrentTime) import Data.Time.Clock.System (getSystemTime) import Data.Word (Word16) +import qualified Database.SQLite.Simple as SQL import Network.Socket (HostName) import Simplex.FileTransfer.Client (XFTPChunkSpec (..), XFTPClient, XFTPClientConfig (..), XFTPClientError) import qualified Simplex.FileTransfer.Client as X @@ -1621,10 +1622,16 @@ withStore :: AgentClient -> (DB.Connection -> IO (Either StoreError a)) -> AM a withStore c action = do st <- asks store withExceptT storeError . ExceptT . liftIO . agentOperationBracket c AODatabase (\_ -> pure ()) $ - withTransaction st action `E.catch` handleInternal "" + withTransaction st action `E.catches` handleDBErrors where - handleInternal :: String -> E.SomeException -> IO (Either StoreError a) - handleInternal ctxStr e = pure . Left . SEInternal . B.pack $ show e <> ctxStr + handleDBErrors :: [E.Handler IO (Either StoreError a)] + handleDBErrors = + [ E.Handler $ \(e :: SQL.SQLError) -> + let se = SQL.sqlError e + busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked + in pure . Left . (if busy then SEDatabaseBusy else SEInternal) $ bshow se, + E.Handler $ \(E.SomeException e) -> pure . Left $ SEInternal $ bshow e + ] withStoreBatch :: Traversable t => AgentClient -> (DB.Connection -> t (IO (Either AgentErrorType a))) -> AM' (t (Either AgentErrorType a)) withStoreBatch c actions = do @@ -1652,6 +1659,7 @@ storeError = \case -- it is used to wrap agent operations when "transaction-like" store access is needed -- NOTE: network IO should NOT be used inside AgentStoreMonad SEAgentError e -> e + SEDatabaseBusy e -> CRITICAL True $ B.unpack e e -> INTERNAL $ show e incStat :: AgentClient -> Int -> AgentStatsKey -> STM () diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index ce76d5c89..b3decd8f0 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -30,7 +30,7 @@ import Data.Type.Equality import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval (RI2State) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (RatchetX448, PQEncryption, PQSupport) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport, RatchetX448) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol ( MsgBody, @@ -593,6 +593,8 @@ type AsyncCmdId = Int64 data StoreError = -- | IO exceptions in store actions. SEInternal ByteString + | -- | Database busy + SEDatabaseBusy ByteString | -- | Failed to generate unique random ID SEUniqueID | -- | User ID not found diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index b8b1c7c52..beac334fb 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -221,6 +221,7 @@ module Simplex.Messaging.Agent.Store.SQLite ) where +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class @@ -268,7 +269,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations (DownMigration (..), MTRE import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) -import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys, PQEncryption (..), PQSupport (..)) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -278,7 +279,7 @@ import Simplex.Messaging.Parsers (blobFieldParser, defaultJSON, dropPrefix, from import Simplex.Messaging.Protocol import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost) -import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, ($>>=), (<$$>)) +import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, tshow, ($>>=), (<$$>)) import Simplex.Messaging.Version.Internal import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist) import System.Exit (exitFailure) @@ -1272,12 +1273,16 @@ createCommand :: DB.Connection -> ACorrId -> ConnId -> Maybe SMPServer -> AgentC createCommand db corrId connId srv_ cmd = runExceptT $ do (host_, port_, serverKeyHash_) <- serverFields createdAt <- liftIO getCurrentTime - liftIO $ + liftIO . E.handle handleErr $ DB.execute db "INSERT INTO commands (host, port, corr_id, conn_id, command_tag, command, server_key_hash, created_at) VALUES (?,?,?,?,?,?,?,?)" - (host_, port_, corrId, connId, agentCommandTag cmd, cmd, serverKeyHash_, createdAt) + (host_, port_, corrId, connId, cmdTag, cmd, serverKeyHash_, createdAt) where + cmdTag = agentCommandTag cmd + handleErr e + | SQL.sqlError e == SQL.ErrorConstraint = logError $ "tried to create command " <> tshow cmdTag <> " for deleted connection" + | otherwise = E.throwIO e serverFields :: ExceptT StoreError IO (Maybe (NonEmpty TransportHost), Maybe ServiceName, Maybe C.KeyHash) serverFields = case srv_ of Just srv@(SMPServer host port _) -> From 969951d963a6b326b01984de55831b612b0cbdbe Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Mon, 13 May 2024 09:46:37 +0000 Subject: [PATCH 036/125] actions: ignore uploading GHC 8.10.7 binaries (#1064) In other words, finally upload 22.04 binaries. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4c44cff9..23fda6a0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}} - name: Build changelog - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04' id: build_changelog uses: mikepenz/release-changelog-builder-action@v1 with: @@ -75,7 +75,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' && matrix.ghc == '9.6.3' + if: startsWith(github.ref, 'refs/tags/v') && matrix.ghc != '8.10.7' uses: softprops/action-gh-release@v1 with: body: | From 512afa1e2bc721241a78e8cc9feaf68b5775d919 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 13 May 2024 15:16:20 +0100 Subject: [PATCH 037/125] agent: count received duplicate messages (#1148) * agent: count received duplicate messages * count total too * names * fix * tuple --- src/Simplex/Messaging/Agent.hs | 20 ++++++++++++++++++++ src/Simplex/Messaging/Agent/Client.hs | 3 +++ 2 files changed, 23 insertions(+) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 198fabdba..f7280facc 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -113,6 +113,7 @@ module Simplex.Messaging.Agent debugAgentLocks, getAgentStats, resetAgentStats, + getMsgCounts, getAgentSubscriptions, logConnection, ) @@ -554,6 +555,9 @@ resetAgentStats :: AgentClient -> IO () resetAgentStats = atomically . TM.clear . agentStats {-# INLINE resetAgentStats #-} +getMsgCounts :: AgentClient -> IO [(ConnId, (Int, Int))] -- (total, duplicates) +getMsgCounts c = readTVarIO (msgCounts c) >>= mapM (\(connId, cnt) -> (connId,) <$> readTVarIO cnt) . M.assocs + withAgentEnv' :: AgentClient -> AM' a -> IO a withAgentEnv' c = (`runReaderT` agentEnv c) {-# INLINE withAgentEnv' #-} @@ -2135,6 +2139,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, _ -> pure () let encryptedMsgHash = C.sha256Hash encAgentMessage g <- asks random + atomically updateTotalMsgCount tryError (agentClientMsg g encryptedMsgHash) >>= \case Right (Just (msgId, msgMeta, aMessage, rcPrev)) -> do conn'' <- resetRatchetSync @@ -2168,6 +2173,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, | otherwise = pure conn' Right _ -> prohibited >> ack Left e@(AGENT A_DUPLICATE) -> do + atomically updateDupMsgCount withStore' c (\db -> getLastMsg db connId srvMsgId) >>= \case Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck} | userAck -> ackDel internalId @@ -2198,6 +2204,20 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, checkDuplicateHash e encryptedMsgHash = unlessM (withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash) $ throwError e + updateTotalMsgCount :: STM () + updateTotalMsgCount = + TM.lookup connId (msgCounts c) >>= \case + Just v -> modifyTVar' v $ first (+ 1) + Nothing -> addMsgCount 0 + updateDupMsgCount :: STM () + updateDupMsgCount = + TM.lookup connId (msgCounts c) >>= \case + Just v -> modifyTVar' v $ second (+ 1) + Nothing -> addMsgCount 1 + addMsgCount :: Int -> STM () + addMsgCount duplicate = do + counts <- newTVar (1, duplicate) + TM.insert connId counts (msgCounts c) agentClientMsg :: TVar ChaChaDRG -> ByteString -> AM (Maybe (InternalId, MsgMeta, AMessage, CR.RatchetX448)) agentClientMsg g encryptedMsgHash = withStore c $ \db -> runExceptT $ do rc <- ExceptT $ getRatchet db connId -- ratchet state pre-decryption - required for processing EREADY diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 4c359a519..0e4a5b49e 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -298,6 +298,7 @@ data AgentClient = AgentClient -- smpSubWorkers for SMP servers sessions smpSubWorkers :: TMap SMPTransportSession (SessionVar (Async ())), agentStats :: TMap AgentStatsKey (TVar Int), + msgCounts :: TMap ConnId (TVar (Int, Int)), -- (total, duplicates) clientId :: Int, agentEnv :: Env } @@ -459,6 +460,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = deleteLock <- createLock smpSubWorkers <- TM.empty agentStats <- TM.empty + msgCounts <- TM.empty return AgentClient { acThread, @@ -494,6 +496,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = deleteLock, smpSubWorkers, agentStats, + msgCounts, clientId, agentEnv } From 762909ce33c01d71bbabfed156e1e9faeebbedca Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 13 May 2024 20:35:46 +0100 Subject: [PATCH 038/125] 5.8.0.0 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 02a088e23..0aed0c806 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.4.0 +version: 5.8.0.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 3366cb0b8..92b6a9cfb 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.4.0 +version: 5.8.0.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From f51cf1deacd14aced88652ccfa06a746999e2ac6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 14 May 2024 20:04:51 +0100 Subject: [PATCH 039/125] agent: use MVar for DB connection for more fair connection distribution between threads (#1147) --- src/Simplex/Messaging/Agent/Store/SQLite.hs | 9 ++++---- .../Messaging/Agent/Store/SQLite/Common.hs | 21 ++++++++----------- tests/AgentTests/SQLiteTests.hs | 7 ++++--- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index beac334fb..e47d2a15c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -287,6 +287,7 @@ import System.FilePath (takeDirectory) import System.IO (hFlush, stdout) import UnliftIO.Exception (bracketOnError, onException) import qualified UnliftIO.Exception as E +import UnliftIO.MVar import UnliftIO.STM -- * SQLite Store implementation @@ -382,8 +383,8 @@ connectSQLiteStore :: FilePath -> ScrubbedBytes -> Bool -> IO SQLiteStore connectSQLiteStore dbFilePath key keepKey = do dbNew <- not <$> doesFileExist dbFilePath dbConn <- dbBusyLoop (connectDB dbFilePath key) + dbConnection <- newMVar dbConn atomically $ do - dbConnection <- newTMVar dbConn dbKey <- newTVar $! storeKey key keepKey dbClosed <- newTVar False pure SQLiteStore {dbFilePath, dbKey, dbConnection, dbNew, dbClosed} @@ -421,14 +422,14 @@ openSQLiteStore st@SQLiteStore {dbClosed} key keepKey = openSQLiteStore_ :: SQLiteStore -> ScrubbedBytes -> Bool -> IO () openSQLiteStore_ SQLiteStore {dbConnection, dbFilePath, dbKey, dbClosed} key keepKey = bracketOnError - (atomically $ takeTMVar dbConnection) - (atomically . tryPutTMVar dbConnection) + (takeMVar dbConnection) + (tryPutMVar dbConnection) $ \DB.Connection {slow} -> do DB.Connection {conn} <- connectDB dbFilePath key atomically $ do - putTMVar dbConnection DB.Connection {conn, slow} writeTVar dbClosed False writeTVar dbKey $! storeKey key keepKey + putMVar dbConnection DB.Connection {conn, slow} reopenSQLiteStore :: SQLiteStore -> IO () reopenSQLiteStore st@SQLiteStore {dbKey, dbClosed} = diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs index 18c16cc8b..b9a9bd501 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs @@ -22,8 +22,8 @@ import Database.SQLite.Simple (SQLError) import qualified Database.SQLite.Simple as SQL import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Util (diffToMilliseconds) -import UnliftIO.Exception (bracket) import qualified UnliftIO.Exception as E +import UnliftIO.MVar import UnliftIO.STM storeKey :: ScrubbedBytes -> Bool -> Maybe ScrubbedBytes @@ -32,16 +32,13 @@ storeKey key keepKey = if keepKey || BA.null key then Just key else Nothing data SQLiteStore = SQLiteStore { dbFilePath :: FilePath, dbKey :: TVar (Maybe ScrubbedBytes), - dbConnection :: TMVar DB.Connection, + dbConnection :: MVar DB.Connection, dbClosed :: TVar Bool, dbNew :: Bool } withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a -withConnection SQLiteStore {dbConnection} = - bracket - (atomically $ takeTMVar dbConnection) - (atomically . putTMVar dbConnection) +withConnection SQLiteStore {dbConnection} = withMVar dbConnection withConnection' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a withConnection' st action = withConnection st $ action . DB.conn @@ -71,9 +68,9 @@ dbBusyLoop action = loop 500 3000000 loop :: Int -> Int -> IO a loop t tLim = action `E.catch` \(e :: SQLError) -> - let se = SQL.sqlError e in - if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked) - then do - threadDelay t - loop (t * 9 `div` 8) (tLim - t) - else E.throwIO e + let se = SQL.sqlError e + in if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked) + then do + threadDelay t + loop (t * 9 `div` 8) (tLim - t) + else E.throwIO e diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 4bac4fb83..436dd0eca 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -5,8 +5,8 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -17,6 +17,7 @@ module AgentTests.SQLiteTests (storeTests) where import AgentTests.EqInstances () import Control.Concurrent.Async (concurrently_) +import Control.Concurrent.MVar import Control.Concurrent.STM import Control.Exception (SomeException) import Control.Monad (replicateM_) @@ -45,9 +46,9 @@ import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Protocol (SubscriptionMode (..), pattern VersionSMPC) import qualified Simplex.Messaging.Protocol as SMP @@ -88,7 +89,7 @@ removeStore db = do removeFile $ dbFilePath db where close :: SQLiteStore -> IO () - close st = mapM_ DB.close =<< atomically (tryTakeTMVar $ dbConnection st) + close st = mapM_ DB.close =<< tryTakeMVar (dbConnection st) storeTests :: Spec storeTests = do From 6d190333751c8157cf95736c8bb3aa69cb956f2b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 15 May 2024 12:06:42 +0100 Subject: [PATCH 040/125] proxy: remember server connection error for some time --- src/Simplex/Messaging/Client/Agent.hs | 37 ++++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index d32d59b3f..e54c6e5ff 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} @@ -33,6 +34,7 @@ import qualified Data.Map.Strict as M import Data.Maybe (listToMaybe) import Data.Set (Set) import Data.Text.Encoding +import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime) import Data.Tuple (swap) import Numeric.Natural import Simplex.Messaging.Agent.RetryInterval @@ -44,14 +46,14 @@ import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport -import Simplex.Messaging.Util (catchAll_, toChunks, ($>>=)) +import Simplex.Messaging.Util (catchAll_, ifM, toChunks, ($>>=)) import System.Timeout (timeout) import UnliftIO (async) import UnliftIO.Exception (Exception) import qualified UnliftIO.Exception as E import UnliftIO.STM -type SMPClientVar = SessionVar (Either SMPClientError SMPClient) +type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) SMPClient) data SMPClientAgentEvent = CAConnected SMPServer @@ -70,6 +72,7 @@ type SMPSub = (SMPSubParty, QueueId) data SMPClientAgentConfig = SMPClientAgentConfig { smpCfg :: ProtocolClientConfig SMPVersion, reconnectInterval :: RetryInterval, + persistErrorInterval :: NominalDiffTime, msgQSize :: Natural, agentQSize :: Natural, agentSubsBatchSize :: Int @@ -85,6 +88,7 @@ defaultSMPClientAgentConfig = increaseAfter = 10 * second, maxInterval = 10 * second }, + persistErrorInterval = 0, msgQSize = 256, agentQSize = 256, agentSubsBatchSize = 900 @@ -168,10 +172,15 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, waitForSMPClient v = do let ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) - liftEither $ case smpClient_ of - Just (Right smpClient) -> Right smpClient - Just (Left e) -> Left e - Nothing -> Left PCEResponseTimeout + case smpClient_ of + Just (Right smpClient) -> pure smpClient + Just (Left (e, Nothing)) -> throwE e + Just (Left (e, Just ts)) -> + ifM + ((ts <) <$> liftIO getCurrentTime) + (atomically (removeSessVar v srv smpClients) >> getSMPServerClient' ca srv) + (throwE e) + Nothing -> throwE PCEResponseTimeout newSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient newSMPClient v = tryConnectClient pure (liftIO tryConnectAsync) @@ -182,15 +191,19 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, Right smp -> do logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv atomically $ do - putTMVar (sessionVar v) r + putTMVar (sessionVar v) (Right smp) TM.insert (sessionId $ thParams smp) smp smpSessions successAction smp Left e -> do - if e == PCENetworkError || e == PCEResponseTimeout - then retryAction - else atomically $ do - putTMVar (sessionVar v) (Left e) - removeSessVar v srv smpClients + if + | e == PCENetworkError || e == PCEResponseTimeout -> retryAction + | persistErrorInterval agentCfg == 0 -> do + atomically $ do + putTMVar (sessionVar v) (Left (e, Nothing)) + removeSessVar v srv smpClients + | otherwise -> do + ts <- addUTCTime (persistErrorInterval agentCfg) <$> liftIO getCurrentTime + atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) throwE e tryConnectAsync :: IO () tryConnectAsync = do From 16d79da73e8f56792b312d3d1db3d3384f8f987c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 15 May 2024 15:15:59 +0100 Subject: [PATCH 041/125] config --- src/Simplex/Messaging/Server/Main.hs | 3 ++- tests/SMPClient.hs | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 418ef36c8..fa1698653 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -244,7 +244,8 @@ smpServerCLI cfgPath logPath = hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini, requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini } - } + }, + persistErrorInterval = 30 -- seconds }, allowSMPProxy = True } diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 441ece34b..d2e11d29b 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -133,7 +133,11 @@ proxyCfg = cfgV7 { allowSMPProxy = True, smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}} + smpAgentCfg = + defaultSMPClientAgentConfig + { smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}, + persistErrorInterval = 3 -- seconds + } } proxyVRange :: VersionRangeSMP From 426f47c8059e731681e18c6d08d62f74e1646d81 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Thu, 16 May 2024 21:06:27 +0300 Subject: [PATCH 042/125] smp: use session vars for reconnecting small agent (#1152) * smp: use session vars for reconnecting small agent * process errors * split session and protocol functions * add active flag to agent * actually invoke agent shutdown * close proxy agent too * restore stopping ntf subscribers --- src/Simplex/Messaging/Client/Agent.hs | 201 +++++++++--------- src/Simplex/Messaging/Notifications/Server.hs | 10 +- src/Simplex/Messaging/Server.hs | 10 +- 3 files changed, 118 insertions(+), 103 deletions(-) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index e54c6e5ff..e08f7b2c9 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -15,6 +15,7 @@ module Simplex.Messaging.Client.Agent where import Control.Concurrent (forkIO) import Control.Concurrent.Async (Async, uninterruptibleCancel) +import Control.Concurrent.STM (retry) import Control.Logger.Simple import Control.Monad import Control.Monad.Except @@ -46,7 +47,7 @@ import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport -import Simplex.Messaging.Util (catchAll_, ifM, toChunks, ($>>=)) +import Simplex.Messaging.Util (catchAll_, ifM, toChunks, whenM, ($>>=)) import System.Timeout (timeout) import UnliftIO (async) import UnliftIO.Exception (Exception) @@ -58,7 +59,6 @@ type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) SMPClient data SMPClientAgentEvent = CAConnected SMPServer | CADisconnected SMPServer (Set SMPSub) - | CAReconnected SMPServer | CAResubscribed SMPServer (NonEmpty SMPSub) | CASubError SMPServer (NonEmpty (SMPSub, SMPClientError)) @@ -98,6 +98,7 @@ defaultSMPClientAgentConfig = data SMPClientAgent = SMPClientAgent { agentCfg :: SMPClientAgentConfig, + active :: TVar Bool, msgQ :: TBQueue (ServerTransmission SMPVersion ErrorType BrokerMsg), agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, @@ -105,8 +106,7 @@ data SMPClientAgent = SMPClientAgent smpSessions :: TMap SessionId SMPClient, srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), - reconnections :: TVar [Async ()], - asyncClients :: TVar [Async ()], + smpSubWorkers :: TMap SMPServer (SessionVar (Async ())), workerSeq :: TVar Int } @@ -137,18 +137,19 @@ instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do + active <- newTVar True msgQ <- newTBQueue msgQSize agentQ <- newTBQueue agentQSize smpClients <- TM.empty smpSessions <- TM.empty srvSubs <- TM.empty pendingSrvSubs <- TM.empty - reconnections <- newTVar [] - asyncClients <- newTVar [] + smpSubWorkers <- TM.empty workerSeq <- newTVar 0 pure SMPClientAgent { agentCfg, + active, msgQ, agentQ, randomDrg, @@ -156,14 +157,14 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg smpSessions, srvSubs, pendingSrvSubs, - reconnections, - asyncClients, + smpSubWorkers, workerSeq } +-- | Get or create SMP client for SMPServer getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient -getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, workerSeq} srv = - atomically getClientVar >>= either newSMPClient waitForSMPClient +getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv = + atomically getClientVar >>= either (ExceptT . newSMPClient) waitForSMPClient where getClientVar :: STM (Either SMPClientVar SMPClientVar) getClientVar = getSessVar workerSeq srv smpClients @@ -182,48 +183,39 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, (throwE e) Nothing -> throwE PCEResponseTimeout - newSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient - newSMPClient v = tryConnectClient pure (liftIO tryConnectAsync) - where - tryConnectClient :: (SMPClient -> ExceptT SMPClientError IO a) -> ExceptT SMPClientError IO () -> ExceptT SMPClientError IO a - tryConnectClient successAction retryAction = - tryE (connectClient v) >>= \r -> case r of - Right smp -> do - logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv - atomically $ do - putTMVar (sessionVar v) (Right smp) - TM.insert (sessionId $ thParams smp) smp smpSessions - successAction smp - Left e -> do - if - | e == PCENetworkError || e == PCEResponseTimeout -> retryAction - | persistErrorInterval agentCfg == 0 -> do - atomically $ do - putTMVar (sessionVar v) (Left (e, Nothing)) - removeSessVar v srv smpClients - | otherwise -> do - ts <- addUTCTime (persistErrorInterval agentCfg) <$> liftIO getCurrentTime - atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) - throwE e - tryConnectAsync :: IO () - tryConnectAsync = do - a <- async $ void $ runExceptT connectAsync - atomically $ modifyTVar' (asyncClients ca) (a :) - connectAsync :: ExceptT SMPClientError IO () - connectAsync = - withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> - void $ tryConnectClient (const reconnectClient) loop + newSMPClient :: SMPClientVar -> IO (Either SMPClientError SMPClient) + newSMPClient v = do + r <- connectClient ca srv v `E.catch` (pure . Left . PCEIOError) + case r of + Right smp -> do + logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv + atomically $ do + putTMVar (sessionVar v) (Right smp) + TM.insert (sessionId $ thParams smp) smp smpSessions + notify ca $ CAConnected srv + Left e -> do + if persistErrorInterval agentCfg == 0 || e == PCENetworkError || e == PCEResponseTimeout + then atomically $ do + putTMVar (sessionVar v) (Left (e, Nothing)) + removeSessVar v srv smpClients + else do + ts <- addUTCTime (persistErrorInterval agentCfg) <$> liftIO getCurrentTime + atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) + reconnectClient ca srv + pure r - connectClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient - connectClient v = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) (clientDisconnected v) - - clientDisconnected :: SMPClientVar -> SMPClient -> IO () - clientDisconnected v smp = do - removeClientAndSubs v smp >>= (`forM_` serverDown) +-- | Run an SMP client for SMPClientVar +connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient) +connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg} srv v = + getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected + where + clientDisconnected :: SMPClient -> IO () + clientDisconnected smp = do + removeClientAndSubs smp >>= (`forM_` serverDown) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv - removeClientAndSubs :: SMPClientVar -> SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) - removeClientAndSubs v smp = atomically $ do + removeClientAndSubs :: SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) + removeClientAndSubs smp = atomically $ do removeSessVar v srv smpClients TM.delete (sessionId $ thParams smp) smpSessions TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs @@ -241,63 +233,82 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, serverDown :: Map SMPSub C.APrivateAuthKey -> IO () serverDown ss = unless (M.null ss) $ do - notify . CADisconnected srv $ M.keysSet ss - reconnectServer + notify ca . CADisconnected srv $ M.keysSet ss + reconnectClient ca srv - reconnectServer :: IO () - reconnectServer = do - a <- async $ void $ runExceptT tryReconnectClient - atomically $ modifyTVar' (reconnections ca) (a :) +-- | Spawn reconnect worker if needed +reconnectClient :: SMPClientAgent -> SMPServer -> IO () +reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} srv = + whenM (readTVarIO active) $ atomically getWorkerVar >>= mapM_ (either newSubWorker (\_ -> pure ())) + where + getWorkerVar = + ifM + (null <$> getPending) + (pure Nothing) -- prevent race with cleanup and adding pending queues in another call + (Just <$> getSessVar workerSeq srv smpSubWorkers) + newSubWorker :: SessionVar (Async ()) -> IO () + newSubWorker v = do + a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v) + atomically $ putTMVar (sessionVar v) a + runSubWorker = + withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> do + pending <- atomically getPending + forM_ pending $ \cs -> whenM (readTVarIO active) $ do + void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv cs) + loop + ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg + getPending = mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca) + cleanup :: SessionVar (Async ()) -> STM () + cleanup v = do + -- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar. + -- Not waiting may result in terminated worker remaining in the map. + whenM (isEmptyTMVar $ sessionVar v) retry + removeSessVar v srv smpSubWorkers - tryReconnectClient :: ExceptT SMPClientError IO () - tryReconnectClient = do - withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> - reconnectClient `catchE` const loop - - reconnectClient :: ExceptT SMPClientError IO () - reconnectClient = do - withSMP ca srv $ \smp -> do - liftIO $ notify $ CAReconnected srv - cs_ <- atomically $ mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca) - forM_ cs_ $ \cs -> do - subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs - let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs' - subscribe_ smp SPNotifier nSubs - subscribe_ smp SPRecipient rSubs +reconnectSMPClient :: SMPClientAgent -> SMPServer -> Map SMPSub C.APrivateAuthKey -> ExceptT SMPClientError IO () +reconnectSMPClient ca@SMPClientAgent {agentCfg} srv cs = + withSMP ca srv $ \smp -> do + subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs + let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs' + subscribe_ smp SPNotifier nSubs + subscribe_ smp SPRecipient rSubs + where + isNotifier = \case + SPNotifier -> True + SPRecipient -> False + subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO () + subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg) where - isNotifier = \case - SPNotifier -> True - SPRecipient -> False + subscribeBatch subs' = do + let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs' + rs <- liftIO $ smpSubscribeQueues party ca smp srv subs'' + let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) = + L.zipWith (first . const) subs' rs + rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] = + map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs' + (errs, oks) = partitionEithers rs'' + (tempErrs, finalErrs) = partition (temporaryClientError . snd) errs + mapM_ (atomically . addSubscription ca srv) oks + mapM_ (notify ca . CAResubscribed srv) $ L.nonEmpty $ map fst oks + mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs + mapM_ (notify ca . CASubError srv) $ L.nonEmpty finalErrs + mapM_ (throwE . snd) $ listToMaybe tempErrs - subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO () - subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg) - where - subscribeBatch subs' = do - let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs' - rs <- liftIO $ smpSubscribeQueues party ca smp srv subs'' - let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) = - L.zipWith (first . const) subs' rs - rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] = - map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs' - (errs, oks) = partitionEithers rs'' - (tempErrs, finalErrs) = partition (temporaryClientError . snd) errs - mapM_ (atomically . addSubscription ca srv) oks - mapM_ (liftIO . notify . CAResubscribed srv) $ L.nonEmpty $ map fst oks - mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs - mapM_ (liftIO . notify . CASubError srv) $ L.nonEmpty finalErrs - mapM_ (throwE . snd) $ listToMaybe tempErrs - - notify :: SMPClientAgentEvent -> IO () - notify evt = atomically $ writeTBQueue (agentQ ca) evt +notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m () +notify ca evt = atomically $ writeTBQueue (agentQ ca) evt +{-# INLINE notify #-} lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe SMPClient) lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions closeSMPClientAgent :: SMPClientAgent -> IO () closeSMPClientAgent c = do + atomically $ writeTVar (active c) False closeSMPServerClients c - cancelActions $ reconnections c - cancelActions $ asyncClients c + atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect + where + cancelReconnect :: SessionVar (Async ()) -> IO () + cancelReconnect v = void . forkIO $ atomically (readTMVar $ sessionVar v) >>= uninterruptibleCancel closeSMPServerClients :: SMPClientAgent -> IO () closeSMPServerClients c = atomically (smpClients c `swapTVar` M.empty) >>= mapM_ (forkIO . closeClient) diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 37eff1e94..5e4cf839e 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -98,7 +98,9 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do stopServer = do withNtfLog closeStoreLog saveServerStats - asks (smpSubscribers . subscriber) >>= readTVarIO >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (liftIO . deRefWeak >=> mapM_ killThread)) + NtfSubscriber {smpSubscribers, smpAgent} <- asks subscriber + liftIO $ readTVarIO smpSubscribers >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (deRefWeak >=> mapM_ killThread)) + liftIO $ closeSMPClientAgent smpAgent serverStatsThread_ :: NtfServerConfig -> [M ()] serverStatsThread_ NtfServerConfig {logStatsInterval = Just interval, logStatsStartTime, serverStatsLogFile} = @@ -239,14 +241,14 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge receiveAgent = forever $ atomically (readTBQueue agentQ) >>= \case - CAConnected _ -> pure () + CAConnected srv -> + logInfo $ "SMP server reconnected " <> showServer' srv CADisconnected srv subs -> do logSubStatus srv "disconnected" $ length subs forM_ subs $ \(_, ntfId) -> do let smpQueue = SMPQueueNtf srv ntfId updateSubStatus smpQueue NSInactive - CAReconnected srv -> - logInfo $ "SMP server reconnected " <> showServer' srv + CAResubscribed srv subs -> do forM_ subs $ \(_, ntfId) -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive logSubStatus srv "resubscribed" $ length subs diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 33163664a..6c8a18aa5 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -69,7 +69,7 @@ import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError) -import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', lookupSMPServerClient) +import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient', lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -136,7 +136,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do : receiveFromProxyAgent pa : map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg ) - `finally` withLock' (savingLock s) "final" (saveServer False) + `finally` withLock' (savingLock s) "final" (saveServer False >> closeServer) where runServer :: (ServiceName, ATransport) -> M () runServer (tcpPort, ATransport t) = do @@ -150,6 +150,9 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do saveServer :: Bool -> M () saveServer keepMsgs = withLog closeStoreLog >> saveServerMessages keepMsgs >> saveServerStats + closeServer :: M () + closeServer = asks (smpAgent . proxyAgent) >>= liftIO . closeSMPClientAgent + serverThread :: forall s. Server -> @@ -193,7 +196,6 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do CAConnected srv -> logInfo $ "SMP server connected " <> showServer' srv CADisconnected srv [] -> logInfo $ "SMP server disconnected " <> showServer' srv CADisconnected srv subs -> logError $ "SMP server disconnected " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) - CAReconnected srv -> logInfo $ "SMP server reconnected " <> showServer' srv CAResubscribed srv subs -> logError $ "SMP server resubscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) CASubError srv errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs) where @@ -658,7 +660,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey Nothing -> ERR $ transportErr TENoServerAuth - _ -> ERR $ transportErr TEVersion + _ -> ERR $ transportErr TEVersion PFWD fwdV pubKey encBlock -> do ProxyAgent {smpAgent} <- asks proxyAgent atomically (lookupSMPServerClient smpAgent sessId) >>= \case From b33e8f4370fb22c31395736250fa016b7a844b90 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 17 May 2024 17:23:59 +0400 Subject: [PATCH 043/125] agent: reconnect xftp clients (#1156) --- src/Simplex/Messaging/Agent.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index f93d03e35..03dd22901 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -445,6 +445,7 @@ setUserNetworkInfo c@AgentClient {userNetworkState} UserNetworkInfo {networkType reconnectAllServers :: AgentClient -> IO () reconnectAllServers c = do reconnectServerClients c smpClients + reconnectServerClients c xftpClients reconnectServerClients c ntfClients -- | Register device notifications token From 33f6d2f1da614851fe04fc40e9bd5c7a43d8e3b5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 17 May 2024 15:12:05 +0100 Subject: [PATCH 044/125] agent: optimize waiting for user network to avoid contention for state updates from multiple threads (#1155) * haskell magic * update * agent: optimize waiting for user network with TChan * clean up * Int64 * use TVar * cleanup * fix * testing * update --- src/Simplex/FileTransfer/Agent.hs | 8 +- src/Simplex/Messaging/Agent.hs | 32 ++--- src/Simplex/Messaging/Agent/Client.hs | 109 ++++++++---------- .../Messaging/Agent/NtfSubSupervisor.hs | 4 +- src/Simplex/Messaging/Util.hs | 14 ++- tests/AgentTests/FunctionalAPITests.hs | 37 +++++- 6 files changed, 115 insertions(+), 89 deletions(-) diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index d04117942..6e948287f 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -180,7 +180,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c downloadFileChunk fc replica `catchAgentError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e where @@ -424,7 +424,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do tryCreate = do usedSrvs <- newTVarIO ([] :: [XFTPServer]) withRetryInterval (riFast ri) $ \_ loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c createWithNextSrv usedSrvs `catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e where @@ -457,7 +457,7 @@ runXFTPSndWorker c srv Worker {doWork} = do fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c uploadFileChunk cfg fc replica `catchAgentError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e where @@ -624,7 +624,7 @@ runXFTPDelWorker c srv Worker {doWork} = do processDeletedReplica replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} = do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c deleteChunkReplica `catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e where diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 03dd22901..12663362d 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -429,18 +429,24 @@ getNetworkConfig = fmap snd . readTVarIO . useNetworkConfig {-# INLINE getNetworkConfig #-} setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO () -setUserNetworkInfo c@AgentClient {userNetworkState} UserNetworkInfo {networkType = nt', online} = withAgentEnv' c $ do - d <- asks $ initialInterval . userNetworkInterval . config - ts <- liftIO getCurrentTime - atomically $ do - ns@UserNetworkState {networkType = nt, offline} <- readTVar userNetworkState - when (nt' /= nt || online /= isNothing offline) $ - writeTVar userNetworkState $! - let offline' - | nt' /= UNNone && online = Nothing - | isJust offline = offline - | otherwise = Just UNSOffline {offlineDelay = d, offlineFrom = ts} - in ns {networkType = nt', offline = offline'} +setUserNetworkInfo c@AgentClient {userNetworkInfo, userNetworkDelay} netInfo = withAgentEnv' c $ do + ni <- asks $ userNetworkInterval . config + let d = initialInterval ni + off <- atomically $ do + wasOnline <- isOnline <$> swapTVar userNetworkInfo netInfo + let off = wasOnline && not (isOnline netInfo) + when off $ writeTVar userNetworkDelay d + pure off + liftIO . when off . void . forkIO $ + growOfflineDelay 0 d ni + where + growOfflineDelay elapsed d ni = do + online <- waitOnlineOrDelay c d + unless online $ do + let elapsed' = elapsed + d + d' = nextRetryDelay elapsed' d ni + atomically $ writeTVar userNetworkDelay d' + growOfflineDelay elapsed' d' ni reconnectAllServers :: AgentClient -> IO () reconnectAllServers c = do @@ -1318,7 +1324,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork let mId = unId msgId ri' = maybe id updateRetryInterval2 msgRetryState ri withRetryLock2 ri' qLock $ \riState loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c resp <- tryError $ case msgType of AM_CONN_INFO -> sendConfirmation c sq msgBody AM_CONN_INFO_REPLY -> sendConfirmation c sq msgBody diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index cfed6b932..1720b2c0f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -103,9 +103,10 @@ module Simplex.Messaging.Agent.Client waitUntilActive, UserNetworkInfo (..), UserNetworkType (..), - UserNetworkState (..), - UNSOffline (..), waitForUserNetwork, + waitOnlineOrDelay, + isNetworkOnline, + isOnline, throwWhenInactive, throwWhenNoDelivery, beginAgentOperation, @@ -165,7 +166,7 @@ import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import Data.Text.Encoding -import Data.Time (UTCTime, defaultTimeLocale, diffUTCTime, formatTime, getCurrentTime) +import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime) import Data.Time.Clock.System (getSystemTime) import Data.Word (Word16) import Network.Socket (HostName) @@ -268,7 +269,8 @@ data AgentClient = AgentClient xftpServers :: TMap UserId (NonEmpty XFTPServerWithAuth), xftpClients :: TMap XFTPTransportSession XFTPClientVar, useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), -- (slow, fast) networks - userNetworkState :: TVar UserNetworkState, + userNetworkInfo :: TVar UserNetworkInfo, + userNetworkDelay :: TVar Int64, subscrConns :: TVar (Set ConnId), activeSubs :: TRcvQueues, pendingSubs :: TRcvQueues, @@ -405,22 +407,20 @@ data UserNetworkInfo = UserNetworkInfo } deriving (Show) +isNetworkOnline :: AgentClient -> STM Bool +isNetworkOnline c = isOnline <$> readTVar (userNetworkInfo c) + +isOnline :: UserNetworkInfo -> Bool +isOnline UserNetworkInfo {networkType, online} = networkType /= UNNone && online + data UserNetworkType = UNNone | UNCellular | UNWifi | UNEthernet | UNOther deriving (Eq, Show) -data UserNetworkState = UserNetworkState - { networkType :: UserNetworkType, - offline :: Maybe UNSOffline - } - deriving (Show) - -data UNSOffline = UNSOffline {offlineDelay :: Int64, offlineFrom :: UTCTime} - deriving (Show) - -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. newAgentClient :: Int -> InitialAgentServers -> Env -> STM AgentClient newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = do - let qSize = tbqSize $ config agentEnv + let cfg = config agentEnv + qSize = tbqSize cfg acThread <- newTVar Nothing active <- newTVar True rcvQ <- newTBQueue qSize @@ -433,7 +433,8 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = xftpServers <- newTVar xftp xftpClients <- TM.empty useNetworkConfig <- newTVar (slowNetworkConfig netCfg, netCfg) - userNetworkState <- newTVar $ UserNetworkState UNOther Nothing + userNetworkInfo <- newTVar $ UserNetworkInfo UNOther True + userNetworkDelay <- newTVar $ initialInterval $ userNetworkInterval cfg subscrConns <- newTVar S.empty activeSubs <- RQ.empty pendingSubs <- RQ.empty @@ -468,7 +469,8 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = xftpServers, xftpClients, useNetworkConfig, - userNetworkState, + userNetworkInfo, + userNetworkDelay, subscrConns, activeSubs, pendingSubs, @@ -612,7 +614,7 @@ resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = withRetryInterval ri $ \_ loop -> do pending <- atomically getPending forM_ (L.nonEmpty pending) $ \qs -> do - waitForUserNetwork c + lift $ waitForUserNetwork c void . tryAgentError' $ reconnectSMPClient timeoutCounts c tSess qs loop getPending = RQ.getSessQueues tSess $ pendingSubs c @@ -629,18 +631,17 @@ reconnectSMPClient tc c tSess@(_, srv, _) qs = do -- this allows 3x of timeout per batch of subscription (90 queues per batch empirically) let t = (length qs `div` 90 + 1) * tcpTimeout * 3 ExceptT (sequence <$> (t `timeout` runExceptT resubscribe)) >>= \case - Just _ -> atomically $ writeTVar tc 0 - Nothing -> - (offline <$> readTVarIO (userNetworkState c)) >>= \case - -- reset and do not report consequitive timeouts while offline - Just _ -> atomically $ writeTVar tc 0 - Nothing -> do - tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) - maxTC <- asks $ maxSubscriptionTimeouts . config - when (tc' >= maxTC) $ do - let msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess - atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL msg) + Just _ -> resetTimeouts + -- reset and do not report consecutive timeouts while offline + Nothing -> ifM (atomically $ isNetworkOnline c) notifyTimeout resetTimeouts where + resetTimeouts = atomically $ writeTVar tc 0 + notifyTimeout = do + tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) + maxTC <- asks $ maxSubscriptionTimeouts . config + when (tc' >= maxTC) $ do + let msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess + atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL msg) resubscribe :: AM () resubscribe = do cs <- readTVarIO $ RQ.getConnections $ activeSubs c @@ -750,43 +751,31 @@ getClientConfig c cfgSel = do getNetworkConfig :: AgentClient -> STM NetworkConfig getNetworkConfig c = do (slowCfg, fastCfg) <- readTVar (useNetworkConfig c) - UserNetworkState {networkType} <- readTVar (userNetworkState c) + UserNetworkInfo {networkType} <- readTVar $ userNetworkInfo c pure $ case networkType of UNCellular -> slowCfg UNNone -> slowCfg _ -> fastCfg -waitForUserNetwork :: AgentClient -> AM' () -waitForUserNetwork AgentClient {userNetworkState} = - readTVarIO userNetworkState >>= mapM_ waitWhileOffline . offline - where - waitWhileOffline UNSOffline {offlineDelay = d} = - unlessM (liftIO $ waitOnline d False) $ do - -- network delay reached, increase delay - ts' <- liftIO getCurrentTime - ni <- asks $ userNetworkInterval . config - atomically $ do - ns@UserNetworkState {offline} <- readTVar userNetworkState - forM_ offline $ \UNSOffline {offlineDelay = d', offlineFrom = ts} -> - -- Using `min` to avoid multiple updates in a short period of time - -- and to reset `offlineDelay` if network went `on` and `off` again. - writeTVar userNetworkState $! - let d'' = nextRetryDelay (diffToMicroseconds $ diffUTCTime ts' ts) (min d d') ni - in ns {offline = Just UNSOffline {offlineDelay = d'', offlineFrom = ts}} - waitOnline :: Int64 -> Bool -> IO Bool - waitOnline t online' - | t <= 0 = pure online' - | otherwise = - registerDelay (fromIntegral maxWait) - >>= atomically . onlineOrDelay - >>= waitOnline (t - maxWait) - where - maxWait = min t $ fromIntegral (maxBound :: Int) - onlineOrDelay delay = do - online <- isNothing . offline <$> readTVar userNetworkState - expired <- readTVar delay - unless (online || expired) retry - pure online +waitForUserNetwork :: AgentClient -> IO () +waitForUserNetwork c = + unlessM (atomically $ isNetworkOnline c) $ + readTVarIO (userNetworkDelay c) >>= void . waitOnlineOrDelay c + +waitOnlineOrDelay :: AgentClient -> Int64 -> IO Bool +waitOnlineOrDelay c t = do + let maxWait = min t $ fromIntegral (maxBound :: Int) + t' = t - maxWait + delay <- registerDelay $ fromIntegral maxWait + online <- + atomically $ do + expired <- readTVar delay + online <- isNetworkOnline c + unless (expired || online) retry + pure online + if online || t' <= 0 + then pure online + else waitOnlineOrDelay c t' closeAgentClient :: AgentClient -> IO () closeAgentClient c = do diff --git a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs index ae0066328..4aaa5f278 100644 --- a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs +++ b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs @@ -161,7 +161,7 @@ runNtfWorker c srv Worker {doWork} = do logInfo $ "runNtfWorker, nextSub " <> tshow nextSub ri <- asks $ reconnectInterval . config withRetryInterval ri $ \_ loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c processSub nextSub `catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show) processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM () @@ -245,7 +245,7 @@ runNtfSMPWorker c srv Worker {doWork} = do logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub ri <- asks $ reconnectInterval . config withRetryInterval ri $ \_ loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c processSub nextSub `catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show) processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM () diff --git a/src/Simplex/Messaging/Util.hs b/src/Simplex/Messaging/Util.hs index a880cfaad..b42d5b378 100644 --- a/src/Simplex/Messaging/Util.hs +++ b/src/Simplex/Messaging/Util.hs @@ -152,12 +152,14 @@ timeoutThrow :: MonadUnliftIO m => e -> Int -> ExceptT e m a -> ExceptT e m a timeoutThrow e ms action = ExceptT (sequence <$> (ms `timeout` runExceptT action)) >>= maybe (throwError e) pure threadDelay' :: Int64 -> IO () -threadDelay' time - | time <= 0 = pure () -threadDelay' time = do - let maxWait = min time $ fromIntegral (maxBound :: Int) - threadDelay $ fromIntegral maxWait - when (maxWait /= time) $ threadDelay' (time - maxWait) +threadDelay' = loop + where + loop time + | time <= 0 = pure () + | otherwise = do + let maxWait = min time $ fromIntegral (maxBound :: Int) + threadDelay $ fromIntegral maxWait + loop $ time - maxWait diffToMicroseconds :: NominalDiffTime -> Int64 diffToMicroseconds diff = fromIntegral ((truncate $ diff * 1000000) :: Integer) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 59e433ea5..16f96bee4 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -50,7 +50,7 @@ module AgentTests.FunctionalAPITests where import AgentTests.ConnectionRequestTests (connReqData, queueAddr, testE2ERatchetParams12) -import Control.Concurrent (killThread, threadDelay) +import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Monad import Control.Monad.Except import Control.Monad.Reader @@ -436,6 +436,7 @@ functionalAPITests t = do describe "user network info" $ do it "should wait for user network" testWaitForUserNetwork it "should not reset offline interval while offline" testDoNotResetOfflineInterval + it "should resume multiple threads" testResumeMultipleThreads testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do @@ -2697,6 +2698,7 @@ testWaitForUserNetwork = do a <- getSMPAgentClient' 1 aCfg initAgentServers testDB noNetworkDelay a setUserNetworkInfo a $ UserNetworkInfo UNNone False + threadDelay 5000 networkDelay a 100000 networkDelay a 150000 networkDelay a 200000 @@ -2717,6 +2719,7 @@ testDoNotResetOfflineInterval = do a <- getSMPAgentClient' 1 aCfg initAgentServers testDB noNetworkDelay a setUserNetworkInfo a $ UserNetworkInfo UNWifi False + threadDelay 5000 networkDelay a 100000 networkDelay a 150000 setUserNetworkInfo a $ UserNetworkInfo UNCellular False @@ -2730,16 +2733,42 @@ testDoNotResetOfflineInterval = do where aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 100000, increaseAfter = 0, maxInterval = 200000}} +testResumeMultipleThreads :: IO () +testResumeMultipleThreads = do + a <- getSMPAgentClient' 1 aCfg initAgentServers testDB + noNetworkDelay a + setUserNetworkInfo a $ UserNetworkInfo UNNone False + vs <- + replicateM 50000 $ do + v <- newEmptyTMVarIO + void . forkIO $ waitNetwork a >>= atomically . putTMVar v + pure v + threadDelay 1000000 + setUserNetworkInfo a $ UserNetworkInfo UNCellular True + ts <- mapM (atomically . readTMVar) vs + print $ minimum ts + print $ maximum ts + print $ sum ts `div` fromIntegral (length ts) + let average = sum ts `div` fromIntegral (length ts) + average < 3000000 `shouldBe` True + maximum ts < 4000000 `shouldBe` True + where + aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 1000000, increaseAfter = 0, maxInterval = 3600_000_000}} + noNetworkDelay :: AgentClient -> IO () -noNetworkDelay a = (10000 >) <$> waitNetwork a `shouldReturn` True +noNetworkDelay a = do + d <- waitNetwork a + unless (d < 10000) $ expectationFailure $ "expected no delay, d = " <> show d networkDelay :: AgentClient -> Int64 -> IO () -networkDelay a d' = (\d -> d' < d && d < d' + 15000) <$> waitNetwork a `shouldReturn` True +networkDelay a d' = do + d <- waitNetwork a + unless (d' < d && d < d' + 15000) $ expectationFailure $ "expected delay " <> show d' <> ", d = " <> show d waitNetwork :: AgentClient -> IO Int64 waitNetwork a = do t <- getCurrentTime - waitForUserNetwork a `runReaderT` agentEnv a + waitForUserNetwork a t' <- getCurrentTime pure $ diffToMicroseconds $ diffUTCTime t' t From 1116aeeea1869e0de38e9faccea76b329b549804 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 17 May 2024 15:39:20 +0100 Subject: [PATCH 045/125] 5.8.0.1 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 0aed0c806..6b90c709f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.0 +version: 5.8.0.1 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index bd0883c23..d077deaae 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.0 +version: 5.8.0.1 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 1bb6a5c43b491f026adcbe87cf08df202ef72a05 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 19 May 2024 07:50:47 +0100 Subject: [PATCH 046/125] agent: do not increase network activity interval while offline (#1159) * agent: do not increase network activity interval while offline * test --- src/Simplex/Messaging/Agent.hs | 26 +++++--------- src/Simplex/Messaging/Agent/Client.hs | 28 ++++----------- src/Simplex/Messaging/Agent/Env/SQLite.hs | 16 +++------ tests/AgentTests/FunctionalAPITests.hs | 43 +++++++++++------------ 4 files changed, 41 insertions(+), 72 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 12663362d..652e2d58d 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -429,24 +429,16 @@ getNetworkConfig = fmap snd . readTVarIO . useNetworkConfig {-# INLINE getNetworkConfig #-} setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO () -setUserNetworkInfo c@AgentClient {userNetworkInfo, userNetworkDelay} netInfo = withAgentEnv' c $ do - ni <- asks $ userNetworkInterval . config - let d = initialInterval ni - off <- atomically $ do - wasOnline <- isOnline <$> swapTVar userNetworkInfo netInfo - let off = wasOnline && not (isOnline netInfo) - when off $ writeTVar userNetworkDelay d - pure off - liftIO . when off . void . forkIO $ - growOfflineDelay 0 d ni +setUserNetworkInfo c@AgentClient {userNetworkInfo, userNetworkUpdated} ni = withAgentEnv' c $ do + ts' <- liftIO getCurrentTime + i <- asks $ userOfflineDelay . config + -- if network offline event happens in less than `userOfflineDelay` after the previous event, it is ignored + atomically . whenM ((isOnline ni ||) <$> notRecentlyChanged ts' i) $ do + writeTVar userNetworkInfo ni + writeTVar userNetworkUpdated $ Just ts' where - growOfflineDelay elapsed d ni = do - online <- waitOnlineOrDelay c d - unless online $ do - let elapsed' = elapsed + d - d' = nextRetryDelay elapsed' d ni - atomically $ writeTVar userNetworkDelay d' - growOfflineDelay elapsed' d' ni + notRecentlyChanged ts' i = + maybe True (\ts -> diffUTCTime ts' ts > i) <$> readTVar userNetworkUpdated reconnectAllServers :: AgentClient -> IO () reconnectAllServers c = do diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 1720b2c0f..2299b2183 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -104,7 +104,6 @@ module Simplex.Messaging.Agent.Client UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork, - waitOnlineOrDelay, isNetworkOnline, isOnline, throwWhenInactive, @@ -155,7 +154,6 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (lefts, partitionEithers) import Data.Functor (($>)) -import Data.Int (Int64) import Data.List (deleteFirstsBy, foldl', partition, (\\)) import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L @@ -270,7 +268,7 @@ data AgentClient = AgentClient xftpClients :: TMap XFTPTransportSession XFTPClientVar, useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), -- (slow, fast) networks userNetworkInfo :: TVar UserNetworkInfo, - userNetworkDelay :: TVar Int64, + userNetworkUpdated :: TVar (Maybe UTCTime), subscrConns :: TVar (Set ConnId), activeSubs :: TRcvQueues, pendingSubs :: TRcvQueues, @@ -434,7 +432,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = xftpClients <- TM.empty useNetworkConfig <- newTVar (slowNetworkConfig netCfg, netCfg) userNetworkInfo <- newTVar $ UserNetworkInfo UNOther True - userNetworkDelay <- newTVar $ initialInterval $ userNetworkInterval cfg + userNetworkUpdated <- newTVar Nothing subscrConns <- newTVar S.empty activeSubs <- RQ.empty pendingSubs <- RQ.empty @@ -470,7 +468,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = xftpClients, useNetworkConfig, userNetworkInfo, - userNetworkDelay, + userNetworkUpdated, subscrConns, activeSubs, pendingSubs, @@ -759,23 +757,9 @@ getNetworkConfig c = do waitForUserNetwork :: AgentClient -> IO () waitForUserNetwork c = - unlessM (atomically $ isNetworkOnline c) $ - readTVarIO (userNetworkDelay c) >>= void . waitOnlineOrDelay c - -waitOnlineOrDelay :: AgentClient -> Int64 -> IO Bool -waitOnlineOrDelay c t = do - let maxWait = min t $ fromIntegral (maxBound :: Int) - t' = t - maxWait - delay <- registerDelay $ fromIntegral maxWait - online <- - atomically $ do - expired <- readTVar delay - online <- isNetworkOnline c - unless (expired || online) retry - pure online - if online || t' <= 0 - then pure online - else waitOnlineOrDelay c t' + unlessM (atomically $ isNetworkOnline c) $ do + delay <- registerDelay $ userNetworkInterval $ config $ agentEnv c + atomically $ unlessM (isNetworkOnline c) $ unlessM (readTVar delay) retry closeAgentClient :: AgentClient -> IO () closeAgentClient c = do diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 07d3f29a8..9613adf3c 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -92,7 +92,8 @@ data AgentConfig = AgentConfig xftpCfg :: XFTPClientConfig, reconnectInterval :: RetryInterval, messageRetryInterval :: RetryInterval2, - userNetworkInterval :: RetryInterval, + userNetworkInterval :: Int, + userOfflineDelay :: NominalDiffTime, messageTimeout :: NominalDiffTime, connDeleteDeliveryTimeout :: NominalDiffTime, helloTimeout :: NominalDiffTime, @@ -147,14 +148,6 @@ defaultMessageRetryInterval = } } -defaultUserNetworkInterval :: RetryInterval -defaultUserNetworkInterval = - RetryInterval - { initialInterval = 1200_000000, -- 20 minutes - increaseAfter = 0, - maxInterval = 7200_000000 -- 2 hours - } - defaultAgentConfig :: AgentConfig defaultAgentConfig = AgentConfig @@ -170,7 +163,8 @@ defaultAgentConfig = xftpCfg = defaultXFTPClientConfig, reconnectInterval = defaultReconnectInterval, messageRetryInterval = defaultMessageRetryInterval, - userNetworkInterval = defaultUserNetworkInterval, + userNetworkInterval = 1800_000000, -- 30 minutes, should be less than Int32 max value + userOfflineDelay = 2, -- if network offline event happens in less than 2 seconds after it was set online, it is ignored messageTimeout = 2 * nominalDay, connDeleteDeliveryTimeout = 2 * nominalDay, helloTimeout = 2 * nominalDay, @@ -179,7 +173,7 @@ defaultAgentConfig = cleanupInterval = 30 * 60 * 1000000, -- 30 minutes cleanupStepInterval = 200000, -- 200ms maxWorkerRestartsPerMin = 5, - -- 3 consecutive subscription timeouts will result in alert to the user + -- 5 consecutive subscription timeouts will result in alert to the user -- this is a fallback, as the timeout set to 3x of expected timeout, to avoid potential locking. maxSubscriptionTimeouts = 5, storedMsgDataTTL = 21 * nominalDay, diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 16f96bee4..7cf1ab00a 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -77,7 +77,6 @@ import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestSte import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore) import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) import qualified Simplex.Messaging.Agent.Protocol as A -import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig) @@ -435,7 +434,7 @@ functionalAPITests t = do it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent t describe "user network info" $ do it "should wait for user network" testWaitForUserNetwork - it "should not reset offline interval while offline" testDoNotResetOfflineInterval + it "should not reset online to offline if happens too quickly" testDoNotResetOnlineToOffline it "should resume multiple threads" testResumeMultipleThreads testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int @@ -2698,11 +2697,8 @@ testWaitForUserNetwork = do a <- getSMPAgentClient' 1 aCfg initAgentServers testDB noNetworkDelay a setUserNetworkInfo a $ UserNetworkInfo UNNone False - threadDelay 5000 networkDelay a 100000 - networkDelay a 150000 - networkDelay a 200000 - networkDelay a 200000 + networkDelay a 100000 setUserNetworkInfo a $ UserNetworkInfo UNCellular True noNetworkDelay a setUserNetworkInfo a $ UserNetworkInfo UNCellular False @@ -2712,26 +2708,29 @@ testWaitForUserNetwork = do (networkDelay a 50000) noNetworkDelay a where - aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 100000, increaseAfter = 0, maxInterval = 200000}} + aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0} -testDoNotResetOfflineInterval :: IO () -testDoNotResetOfflineInterval = do +testDoNotResetOnlineToOffline :: IO () +testDoNotResetOnlineToOffline = do a <- getSMPAgentClient' 1 aCfg initAgentServers testDB noNetworkDelay a setUserNetworkInfo a $ UserNetworkInfo UNWifi False - threadDelay 5000 networkDelay a 100000 - networkDelay a 150000 - setUserNetworkInfo a $ UserNetworkInfo UNCellular False - networkDelay a 200000 - setUserNetworkInfo a $ UserNetworkInfo UNNone False - networkDelay a 200000 - setUserNetworkInfo a $ UserNetworkInfo UNCellular True + setUserNetworkInfo a $ UserNetworkInfo UNWifi False + setUserNetworkInfo a $ UserNetworkInfo UNWifi True noNetworkDelay a - setUserNetworkInfo a $ UserNetworkInfo UNCellular False + setUserNetworkInfo a $ UserNetworkInfo UNWifi False -- ingnored + noNetworkDelay a + threadDelay 100000 + setUserNetworkInfo a $ UserNetworkInfo UNWifi False networkDelay a 100000 + setUserNetworkInfo a $ UserNetworkInfo UNNone False + networkDelay a 100000 + setUserNetworkInfo a $ UserNetworkInfo UNWifi True + setUserNetworkInfo a $ UserNetworkInfo UNNone False -- ingnored + noNetworkDelay a where - aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 100000, increaseAfter = 0, maxInterval = 200000}} + aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0.1} testResumeMultipleThreads :: IO () testResumeMultipleThreads = do @@ -2746,14 +2745,14 @@ testResumeMultipleThreads = do threadDelay 1000000 setUserNetworkInfo a $ UserNetworkInfo UNCellular True ts <- mapM (atomically . readTMVar) vs - print $ minimum ts - print $ maximum ts - print $ sum ts `div` fromIntegral (length ts) + -- print $ minimum ts + -- print $ maximum ts + -- print $ sum ts `div` fromIntegral (length ts) let average = sum ts `div` fromIntegral (length ts) average < 3000000 `shouldBe` True maximum ts < 4000000 `shouldBe` True where - aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 1000000, increaseAfter = 0, maxInterval = 3600_000_000}} + aCfg = agentCfg {userOfflineDelay = 0} noNetworkDelay :: AgentClient -> IO () noNetworkDelay a = do From 8b21f7ef2a0e68e0107b7d1d1982ae0d9ff2f1aa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 20 May 2024 07:56:51 +0100 Subject: [PATCH 047/125] agent: aggregate multiple expired subscription responses into a single UP event (#1160) * agent: aggregate multiple expired subscription responses into a single UP event * clean up * refactor processing of expired responses * refactor * refactor 2 * refactor unexpectedResponse --- src/Simplex/FileTransfer/Client.hs | 14 +- src/Simplex/Messaging/Agent.hs | 133 ++++++++++-------- src/Simplex/Messaging/Agent/Client.hs | 15 +- src/Simplex/Messaging/Client.hs | 98 +++++++------ src/Simplex/Messaging/Client/Agent.hs | 2 +- src/Simplex/Messaging/Notifications/Client.hs | 11 +- src/Simplex/Messaging/Notifications/Server.hs | 41 +++--- tests/SMPProxyTests.hs | 4 +- 8 files changed, 180 insertions(+), 138 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 7875542a6..4fb18c27a 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -14,6 +14,7 @@ module Simplex.FileTransfer.Client where import Control.Logger.Simple import Control.Monad import Control.Monad.Except +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first) import Data.ByteString.Builder (Builder, byteString) @@ -38,6 +39,7 @@ import Simplex.Messaging.Client defaultNetworkConfig, proxyUsername, transportClientConfig, + unexpectedResponse, ) import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C @@ -56,7 +58,7 @@ import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client import Simplex.Messaging.Transport.HTTP2.File -import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM) +import Simplex.Messaging.Util (liftEitherWith, liftError', tshow, whenM) import Simplex.Messaging.Version import UnliftIO import UnliftIO.Directory @@ -228,13 +230,13 @@ createXFTPChunk :: createXFTPChunk c spKey file rcps auth_ = sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case (FRSndIds sId rIds, body) -> noFile body (sId, rIds) - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r addXFTPRecipients :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> NonEmpty C.APublicAuthKey -> ExceptT XFTPClientError IO (NonEmpty RecipientId) addXFTPRecipients c spKey fId rcps = sendXFTPCommand c spKey fId (FADD rcps) Nothing >>= \case (FRRcvIds rIds, body) -> noFile body rIds - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r uploadXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> XFTPChunkSpec -> ExceptT XFTPClientError IO () uploadXFTPChunk c spKey fId chunkSpec = @@ -262,7 +264,7 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec { receiveEncFile chunkPart cbState chunkSpec `catchError` \e -> whenM (doesFileExist filePath) (removeFile filePath) >> throwError e _ -> throwError $ PCEResponseError NO_FILE - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r xftpReqTimeout :: XFTPClientConfig -> Maybe Word32 -> Int xftpReqTimeout cfg@XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout}} chunkSize_ = @@ -286,12 +288,12 @@ pingXFTP c@XFTPClient {thParams} = do (r, _) <- sendXFTPTransmission c t Nothing case r of FRPong -> pure () - _ -> throwError $ PCEUnexpectedResponse $ bshow r + _ -> throwE $ unexpectedResponse r okResponse :: (FileResponse, HTTP2Body) -> ExceptT XFTPClientError IO () okResponse = \case (FROk, body) -> noFile body () - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r -- TODO this currently does not check anything because response size is not set and bodyPart is always Just noFile :: HTTP2Body -> a -> ExceptT XFTPClientError IO a diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 7bcec443b..3096291de 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -159,7 +159,7 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission, TransmissionType (..)) +import Simplex.Messaging.Client (ProtocolClient (..), SMPClientError, ServerTransmission (..), ServerTransmissionBatch, temporaryClientError, unexpectedResponse) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs) import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) @@ -170,7 +170,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..)) import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) -import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM @@ -1998,14 +1998,10 @@ getSMPServer c userId = withUserServers c userId pickServer {-# INLINE getSMPServer #-} subscriber :: AgentClient -> AM' () -subscriber c@AgentClient {subQ, msgQ} = forever $ do +subscriber c@AgentClient {msgQ} = forever $ do t <- atomically $ readTBQueue msgQ agentOperationBracket c AORcvNetwork waitUntilActive $ - tryAgentError' (processSMPTransmission c t) >>= \case - Left e -> do - logError $ tshow e - atomically $ writeTBQueue subQ ("", "", APC SAEConn $ ERR e) - Right _ -> return () + processSMPTransmissions c t cleanupManager :: AgentClient -> AM' () cleanupManager c@AgentClient {subQ} = do @@ -2079,28 +2075,72 @@ cleanupManager c@AgentClient {subQ} = do data ACKd = ACKd | ACKPending --- | make sure to ACK or throw in each message processing branch --- it cannot be finally, unfortunately, as sometimes it needs to be ACK+DEL -processSMPTransmission :: AgentClient -> ServerTransmission SMPVersion ErrorType BrokerMsg -> AM () -processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, sessId, tType, rId, cmd) = do - (rq, SomeConn _ conn) <- withStore c (\db -> getRcvConn db srv rId) - processSMP rq conn $ toConnData conn +-- | Make sure to ACK or throw in each message processing branch +-- It cannot be finally, as sometimes it needs to be ACK+DEL, +-- and sometimes ACK has to be sent from the consumer. +processSMPTransmissions :: AgentClient -> ServerTransmissionBatch SMPVersion ErrorType BrokerMsg -> AM' () +processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) = do + upConnIds <- newTVarIO [] + forM_ ts $ \(entId, t) -> case t of + STEvent msgOrErr -> + withRcvConn entId $ \rq@RcvQueue {connId} conn -> case msgOrErr of + Right msg -> processSMP rq conn (toConnData conn) msg + Left e -> lift $ notifyErr connId e + STResponse (Cmd SRecipient cmd) respOrErr -> + withRcvConn entId $ \rq conn -> case cmd of + -- TODO process expired responses to ACK and DEL + SMP.SUB -> case respOrErr of + Right SMP.OK -> processSubOk rq upConnIds + Right msg@SMP.MSG {} -> do + processSubOk rq upConnIds + processSMP rq conn (toConnData conn) msg + Right r -> processSubErr rq $ unexpectedResponse r + Left e -> unless (temporaryClientError e) $ processSubErr rq e -- timeout/network was already reported + _ -> pure () + STResponse {} -> pure () -- TODO process expired responses to sent messages + STUnexpectedError e -> do + logServer "<--" c srv entId $ "error: " <> bshow e + notifyErr "" e + connIds <- readTVarIO upConnIds + unless (null connIds) $ notify' "" $ UP srv connIds where - processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> AM () + withRcvConn :: SMP.RecipientId -> (forall c. RcvQueue -> Connection c -> AM ()) -> AM' () + withRcvConn rId a = do + tryAgentError' (withStore c $ \db -> getRcvConn db srv rId) >>= \case + Left e -> notify' "" (ERR e) + Right (rq@RcvQueue {connId}, SomeConn _ conn) -> + tryAgentError' (a rq conn) >>= \case + Left e -> notify' connId (ERR e) + Right () -> pure () + processSubOk :: RcvQueue -> TVar [ConnId] -> AM () + processSubOk rq@RcvQueue {connId} upConnIds = + atomically . whenM (isPendingSub connId) $ do + addSubscription c rq + modifyTVar' upConnIds (connId :) + processSubErr :: RcvQueue -> SMPClientError -> AM () + processSubErr rq@RcvQueue {connId} e = do + atomically . whenM (isPendingSub connId) $ failSubscription c rq e + lift $ notifyErr connId e + isPendingSub connId = (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId + notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> ACommand 'Agent e -> m () + notify' connId msg = atomically $ writeTBQueue subQ ("", connId, APC (sAEntity @e) msg) + notifyErr :: ConnId -> SMPClientError -> AM' () + notifyErr connId = notify' connId . ERR . protocolClientError SMP (B.unpack $ strEncode srv) + processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM () processSMP - rq@RcvQueue {e2ePrivKey, e2eDhSecret, status} + rq@RcvQueue {rcvId = rId, e2ePrivKey, e2eDhSecret, status} conn - cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} = - withConnLock c connId "processSMP" $ case cmd of - Right r@(SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId}) -> + cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} + smpMsg = + withConnLock c connId "processSMP" $ case smpMsg of + SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> void . handleNotifyAck $ do - isGET <- atomically $ hasGetLock c rq - unless isGET $ checkExpiredResponse r msg' <- decryptSMPMessage rq msg ack' <- handleNotifyAck $ case msg' of SMP.ClientRcvMsgBody {msgTs = srvTs, msgFlags, msgBody} -> processClientMsg srvTs msgFlags msgBody SMP.ClientRcvMsgQuota {} -> queueDrained >> ack - when isGET $ notify (MSGNTF $ SMP.rcvMessageMeta srvMsgId msg') + whenM (atomically $ hasGetLock c rq) $ + notify (MSGNTF $ SMP.rcvMessageMeta srvMsgId msg') pure ack' where queueDrained = case conn of @@ -2259,29 +2299,23 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, ackDel aId = enqueueCmd (ICAckDel rId srvMsgId aId) $> ACKd handleNotifyAck :: AM ACKd -> AM ACKd handleNotifyAck m = m `catchAgentError` \e -> notify (ERR e) >> ack - Right SMP.END -> - atomically (TM.lookup tSess smpClients $>>= (tryReadTMVar . sessionVar) >>= processEND) - >>= logServer "<--" c srv rId + SMP.END -> + atomically (TM.lookup tSess (smpClients c) $>>= (tryReadTMVar . sessionVar) >>= processEND) + >>= notifyEnd where processEND = \case Just (Right clnt) - | sessId == sessionId (thParams $ connectedClient clnt) -> do - removeSubscription c connId - notify' END - pure "END" - | otherwise -> ignored - _ -> ignored - ignored = pure "END from disconnected client - ignored" - Right (SMP.ERR e) -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e - Right r@SMP.OK -> checkExpiredResponse r - Right r -> unexpected r - Left e -> notify $ ERR $ protocolClientError SMP (B.unpack $ strEncode srv) e + | sessId == sessionId (thParams $ connectedClient clnt) -> + removeSubscription c connId $> True + _ -> pure False + notifyEnd removed + | removed = notify END >> logServer "<--" c srv rId "END" + | otherwise = logServer "<--" c srv rId "END from disconnected client - ignored" + SMP.ERR e -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e + r -> unexpected r where - notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m () - notify = atomically . notify' - - notify' :: forall e. AEntityI e => ACommand 'Agent e -> STM () - notify' msg = writeTBQueue subQ ("", connId, APC (sAEntity @e) msg) + notify :: forall e m. (AEntityI e, MonadIO m) => ACommand 'Agent e -> m () + notify = notify' connId prohibited :: AM () prohibited = notify . ERR $ AGENT A_PROHIBITED @@ -2291,25 +2325,10 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, unexpected :: BrokerMsg -> AM () unexpected r = do - logServer "<--" c srv rId $ "unexpected: " <> bshow cmd + logServer "<--" c srv rId $ "unexpected: " <> bshow r -- TODO add extended information about transmission type once UNEXPECTED has string notify . ERR $ BROKER (B.unpack $ strEncode srv) $ UNEXPECTED (take 32 $ show r) - checkExpiredResponse :: BrokerMsg -> AM () - checkExpiredResponse r = case tType of - TTEvent -> pure () - TTUncorrelatedResponse -> unexpected r - TTExpiredResponse (SMP.Cmd _ cmd') -> case cmd' of - SMP.SUB -> do - added <- - atomically $ - ifM - ((&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId) - (True <$ addSubscription c rq) - (pure False) - when added $ notify $ UP srv [connId] - _ -> pure () - decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> AM (SMP.PrivHeader, AgentMsgEnvelope) decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do clientMsg <- agentCbDecrypt e2eDh cmNonce cmEncBody diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index df6105766..64f8c172f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -41,6 +41,7 @@ module Simplex.Messaging.Agent.Client getQueueMessage, decryptSMPMessage, addSubscription, + failSubscription, addNewQueueSubscription, getSubscriptions, sendConfirmation, @@ -268,7 +269,7 @@ data AgentClient = AgentClient active :: TVar Bool, rcvQ :: TBQueue (ATransmission 'Client), subQ :: TBQueue (ATransmission 'Agent), - msgQ :: TBQueue (ServerTransmission SMPVersion ErrorType BrokerMsg), + msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg), smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), smpClients :: TMap SMPTransportSession SMPClientVar, -- smpProxiedRelays: @@ -1079,7 +1080,7 @@ protocolClientError :: (Show err, Encoding err) => (HostName -> err -> AgentErro protocolClientError protocolError_ host = \case PCEProtocolError e -> protocolError_ host e PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e - PCEUnexpectedResponse r -> BROKER host $ UNEXPECTED $ take 32 $ show r + PCEUnexpectedResponse e -> BROKER host $ UNEXPECTED $ B.unpack e PCEResponseTimeout -> BROKER host TIMEOUT PCENetworkError -> BROKER host NETWORK PCEIncompatibleHost -> BROKER host HOST @@ -1269,9 +1270,8 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> STM () processSubResult c rq@RcvQueue {connId} = \case Left e -> - unless (temporaryClientError e) $ do - RQ.deleteQueue rq (pendingSubs c) - TM.insert (RQ.qKey rq) e (removedSubs c) + unless (temporaryClientError e) $ + failSubscription c rq e Right () -> whenM (hasPendingSubscription c connId) $ addSubscription c rq @@ -1391,6 +1391,11 @@ addSubscription c rq@RcvQueue {connId} = do RQ.addQueue rq $ activeSubs c RQ.deleteQueue rq $ pendingSubs c +failSubscription :: AgentClient -> RcvQueue -> SMPClientError -> STM () +failSubscription c rq e = do + RQ.deleteQueue rq (pendingSubs c) + TM.insert (RQ.qKey rq) e (removedSubs c) + addPendingSubscription :: AgentClient -> RcvQueue -> STM () addPendingSubscription c rq@RcvQueue {connId} = do modifyTVar' (subscrConns c) $ S.insert connId diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 97a2867ca..2e05812cb 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -65,6 +65,7 @@ module Simplex.Messaging.Client ProtocolClientError (..), SMPClientError, ProxyClientError (..), + unexpectedResponse, ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), @@ -80,8 +81,8 @@ module Simplex.Messaging.Client proxyUsername, temporaryClientError, smpProxyError, - ServerTransmission, - TransmissionType (..), + ServerTransmissionBatch, + ServerTransmission (..), ClientCommand, -- * For testing @@ -155,7 +156,7 @@ data PClient v err msg = PClient sentCommands :: TMap CorrId (Request err msg), sndQ :: TBQueue ByteString, rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)), - msgQ :: Maybe (TBQueue (ServerTransmission v err msg)) + msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg)) } smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient @@ -206,10 +207,14 @@ type SMPClient = ProtocolClient SMPVersion ErrorType BrokerMsg -- | Type for client command data type ClientCommand msg = (Maybe C.APrivateAuthKey, EntityId, ProtoCommand msg) --- | Type synonym for transmission from some SPM server queue. -type ServerTransmission v err msg = (TransportSession msg, Version v, SessionId, TransmissionType msg, EntityId, Either (ProtocolClientError err) msg) +-- | Type synonym for transmission from SPM servers. +-- Batch response is presented as a single `ServerTransmissionBatch` tuple. +type ServerTransmissionBatch v err msg = (TransportSession msg, Version v, SessionId, NonEmpty (EntityId, ServerTransmission err msg)) -data TransmissionType msg = TTEvent | TTUncorrelatedResponse | TTExpiredResponse (ProtoCommand msg) +data ServerTransmission err msg + = STEvent (Either (ProtocolClientError err) msg) + | STResponse (ProtoCommand msg) (Either (ProtocolClientError err) msg) + | STUnexpectedError (ProtocolClientError err) data HostMode = -- | prefer (or require) onion hosts when connecting via SOCKS proxy @@ -396,7 +401,7 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId) -- -- A single queue can be used for multiple 'SMPClient' instances, -- as 'SMPServerTransmission' includes server information. -getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmission v err msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) +getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do case chooseTransportHost networkConfig (host srv) of Right useHost -> @@ -498,38 +503,47 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize maxCnt = smpPingCount networkConfig process :: ProtocolClient v err msg -> IO () - process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= mapM_ (processMsg c) + process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= processMsgs c - processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO () - processMsg c@ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr)) - | not $ B.null $ bs corrId = + processMsgs :: ProtocolClient v err msg -> NonEmpty (SignedTransmission err msg) -> IO () + processMsgs c ts = do + tsVar <- newTVarIO [] + mapM_ (processMsg c tsVar) ts + ts' <- readTVarIO tsVar + forM_ msgQ $ \q -> + mapM_ (atomically . writeTBQueue q . serverTransmission c) (L.nonEmpty ts') + + processMsg :: ProtocolClient v err msg -> TVar [(EntityId, ServerTransmission err msg)] -> SignedTransmission err msg -> IO () + processMsg ProtocolClient {client_ = PClient {sentCommands}} tsVar (_, _, (corrId, entId, respOrErr)) + | B.null $ bs corrId = sendMsg $ STEvent clientResp + | otherwise = atomically (TM.lookup corrId sentCommands) >>= \case - Nothing -> sendMsg TTUncorrelatedResponse + Nothing -> sendMsg $ STUnexpectedError unexpected Just Request {entityId, command, pending, responseVar} -> do wasPending <- atomically $ do TM.delete corrId sentCommands ifM (swapTVar pending False) - (True <$ tryPutTMVar responseVar (response entityId)) + (True <$ tryPutTMVar responseVar (if entityId == entId then clientResp else Left unexpected)) (pure False) - unless wasPending $ sendMsg $ if entityId == entId then TTExpiredResponse command else TTUncorrelatedResponse - | otherwise = sendMsg TTEvent + unless wasPending $ sendMsg $ if entityId == entId then STResponse command clientResp else STUnexpectedError unexpected where - response entityId - | entityId == entId = clientResp - | otherwise = Left . PCEUnexpectedResponse $ bshow respOrErr + unexpected = unexpectedResponse respOrErr clientResp = case respOrErr of Left e -> Left $ PCEResponseError e Right r -> case protocolError r of Just e -> Left $ PCEProtocolError e _ -> Right r - sendMsg :: TransmissionType msg -> IO () - sendMsg tType = case msgQ of - Just q -> atomically $ writeTBQueue q $ serverTransmission c tType entId clientResp + sendMsg :: ServerTransmission err msg -> IO () + sendMsg t = case msgQ of + Just _ -> atomically $ modifyTVar' tsVar ((entId, t) :) Nothing -> case clientResp of - Left e -> logError $ "SMP client error: " <> tshow e - Right _ -> logWarn $ "SMP client unprocessed event" + Left e -> logError ("SMP client error: " <> tshow e) + Right _ -> logWarn ("SMP client unprocessed event") + +unexpectedResponse :: Show r => r -> ProtocolClientError err +unexpectedResponse = PCEUnexpectedResponse . B.pack . take 32 . show proxyUsername :: TransportSession msg -> ByteString proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_ @@ -585,7 +599,7 @@ smpProxyError :: SMPClientError -> ErrorType smpProxyError = \case PCEProtocolError e -> PROXY $ PROTOCOL e PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e - PCEUnexpectedResponse s -> PROXY $ BROKER $ UNEXPECTED $ B.unpack $ B.take 32 s + PCEUnexpectedResponse e -> PROXY $ BROKER $ UNEXPECTED $ B.unpack e PCEResponseTimeout -> PROXY $ BROKER TIMEOUT PCENetworkError -> PROXY $ BROKER NETWORK PCEIncompatibleHost -> PROXY $ BROKER HOST @@ -606,7 +620,7 @@ createSMPQueue :: createSMPQueue c (rKey, rpKey) dhKey auth subMode = sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode) >>= \case IDS qik -> pure qik - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Subscribe to the SMP queue. -- @@ -617,7 +631,7 @@ subscribeSMPQueue c@ProtocolClient {client_ = PClient {sendPings}} rpKey rId = d sendSMPCommand c (Just rpKey) rId SUB >>= \case OK -> pure () cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Subscribe to multiple SMP queues batching commands if supported. subscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ())) @@ -637,15 +651,15 @@ processSUBResponse :: SMPClient -> Response ErrorType BrokerMsg -> IO (Either SM processSUBResponse c (Response rId r) = case r of Right OK -> pure $ Right () Right cmd@MSG {} -> writeSMPMessage c rId cmd $> Right () - Right r' -> pure . Left . PCEUnexpectedResponse $ bshow r' + Right r' -> pure . Left $ unexpectedResponse r' Left e -> pure $ Left e writeSMPMessage :: SMPClient -> RecipientId -> BrokerMsg -> IO () -writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c TTEvent rId (Right msg)) (msgQ $ client_ c) +writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c [(rId, STEvent (Right msg))]) (msgQ $ client_ c) -serverTransmission :: ProtocolClient v err msg -> TransmissionType msg -> RecipientId -> Either (ProtocolClientError err) msg -> ServerTransmission v err msg -serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} tType entityId msgOrErr = - (transportSession, thVersion, sessionId, tType, entityId, msgOrErr) +serverTransmission :: ProtocolClient v err msg -> NonEmpty (RecipientId, ServerTransmission err msg) -> ServerTransmissionBatch v err msg +serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} ts = + (transportSession, thVersion, sessionId, ts) -- | Get message from SMP queue. The server returns ERR PROHIBITED if a client uses SUB and GET via the same transport connection for the same queue -- @@ -655,7 +669,7 @@ getSMPMessage c rpKey rId = sendSMPCommand c (Just rpKey) rId GET >>= \case OK -> pure Nothing cmd@(MSG msg) -> liftIO (writeSMPMessage c rId cmd) $> Just msg - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Subscribe to the SMP queue notifications. -- @@ -683,7 +697,7 @@ enableSMPQueueNotifications :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> enableSMPQueueNotifications c rpKey rId notifierKey rcvNtfPublicDhKey = sendSMPCommand c (Just rpKey) rId (NKEY notifierKey rcvNtfPublicDhKey) >>= \case NID nId rcvNtfSrvPublicDhKey -> pure (nId, rcvNtfSrvPublicDhKey) - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Enable notifications for the multiple queues for push notifications server. enableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId, NtfPublicAuthKey, RcvNtfPublicDhKey) -> IO (NonEmpty (Either SMPClientError (NotifierId, RcvNtfPublicDhKey))) @@ -692,7 +706,7 @@ enableSMPQueuesNtfs c qs = L.map process <$> sendProtocolCommands c cs cs = L.map (\(rpKey, rId, notifierKey, rcvNtfPublicDhKey) -> (Just rpKey, rId, Cmd SRecipient $ NKEY notifierKey rcvNtfPublicDhKey)) qs process (Response _ r) = case r of Right (NID nId rcvNtfSrvPublicDhKey) -> Right (nId, rcvNtfSrvPublicDhKey) - Right r' -> Left . PCEUnexpectedResponse $ bshow r' + Right r' -> Left $ unexpectedResponse r' Left e -> Left e -- | Disable notifications for the queue for push notifications server. @@ -714,7 +728,7 @@ sendSMPMessage :: SMPClient -> Maybe SndPrivateAuthKey -> SenderId -> MsgFlags - sendSMPMessage c spKey sId flags msg = sendSMPCommand c spKey sId (SEND flags msg) >>= \case OK -> pure () - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Acknowledge message delivery (server deletes the message). -- @@ -724,7 +738,7 @@ ackSMPMessage c rpKey rId msgId = sendSMPCommand c (Just rpKey) rId (ACK msgId) >>= \case OK -> return () cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Irreversibly suspend SMP queue. -- The existing messages from the queue will still be delivered. @@ -756,7 +770,7 @@ connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, t case supportedClientSMPRelayVRange `compatibleVersion` vr of Nothing -> throwE $ transportErr TEVersion Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r | otherwise = throwE $ PCETransportError TEVersion where tOut = Just $ tcpConnectTimeout + tcpTimeout @@ -862,14 +876,14 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c (_auth, _signed, (_c, _e, cmd)) -> case cmd of Right OK -> pure $ Right () Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay - Right e -> throwE $ PCEUnexpectedResponse $ B.take 32 $ bshow e + Right r' -> throwE $ unexpectedResponse r' Left e -> throwE $ PCEResponseError e _ -> throwE $ PCETransportError TEBadBlock ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left _ -> pure . Left $ ProxyUnexpectedResponse $ take 32 $ show r Left e -> case e of PCEProtocolError e' -> pure . Left $ ProxyProtocolError e' - PCEUnexpectedResponse r -> pure . Left $ ProxyUnexpectedResponse $ B.unpack r + PCEUnexpectedResponse e' -> pure . Left $ ProxyUnexpectedResponse $ B.unpack e' PCEResponseError e' -> pure . Left $ ProxyResponseError e' _ -> throwE e @@ -894,13 +908,13 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' pure fwdResponse - r -> throwE . PCEUnexpectedResponse $ B.take 32 $ bshow r + r -> throwE $ unexpectedResponse r okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case OK -> return () - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r okSMPCommands :: PartyI p => Command p -> SMPClient -> NonEmpty (C.APrivateAuthKey, QueueId) -> IO (NonEmpty (Either SMPClientError ())) okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs @@ -909,7 +923,7 @@ okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs cs = L.map (\(pKey, qId) -> (Just pKey, qId, aCmd)) qs process (Response _ r) = case r of Right OK -> Right () - Right r' -> Left . PCEUnexpectedResponse $ bshow r' + Right r' -> Left $ unexpectedResponse r' Left e -> Left e -- | Send SMP command diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index e08f7b2c9..10b83157f 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -99,7 +99,7 @@ defaultSMPClientAgentConfig = data SMPClientAgent = SMPClientAgent { agentCfg :: SMPClientAgentConfig, active :: TVar Bool, - msgQ :: TBQueue (ServerTransmission SMPVersion ErrorType BrokerMsg), + msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg), agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, smpClients :: TMap SMPServer SMPClientVar, diff --git a/src/Simplex/Messaging/Notifications/Client.hs b/src/Simplex/Messaging/Notifications/Client.hs index cc698b344..32d92faf3 100644 --- a/src/Simplex/Messaging/Notifications/Client.hs +++ b/src/Simplex/Messaging/Notifications/Client.hs @@ -12,7 +12,6 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes) import Simplex.Messaging.Protocol (ErrorType) -import Simplex.Messaging.Util (bshow) type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse @@ -25,7 +24,7 @@ ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> Exc ntfRegisterToken c pKey newTkn = sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case NRTknId tknId dhKey -> pure (tknId, dhKey) - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfVerifyToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> NtfRegCode -> ExceptT NtfClientError IO () ntfVerifyToken c pKey tknId code = okNtfCommand (TVFY code) c pKey tknId @@ -34,7 +33,7 @@ ntfCheckToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> ExceptT NtfClie ntfCheckToken c pKey tknId = sendNtfCommand c (Just pKey) tknId TCHK >>= \case NRTkn stat -> pure stat - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfReplaceToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> DeviceToken -> ExceptT NtfClientError IO () ntfReplaceToken c pKey tknId token = okNtfCommand (TRPL token) c pKey tknId @@ -49,13 +48,13 @@ ntfCreateSubscription :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Subscri ntfCreateSubscription c pKey newSub = sendNtfCommand c (Just pKey) "" (SNEW newSub) >>= \case NRSubId subId -> pure subId - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfCheckSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus ntfCheckSubscription c pKey subId = sendNtfCommand c (Just pKey) subId SCHK >>= \case NRSub stat -> pure stat - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfDeleteSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO () ntfDeleteSubscription = okNtfCommand SDEL @@ -68,4 +67,4 @@ okNtfCommand :: NtfEntityI e => NtfCommand e -> NtfClient -> C.APrivateAuthKey - okNtfCommand cmd c pKey entId = sendNtfCommand c (Just pKey) entId cmd >>= \case NROk -> return () - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 5e4cf839e..892560660 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -31,7 +31,7 @@ import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) import Data.Time.Clock.System (getSystemTime) import Data.Time.Format.ISO8601 (iso8601Show) import Network.Socket (ServiceName) -import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError) +import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError, ServerTransmission (..)) import Simplex.Messaging.Client.Agent import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String @@ -220,23 +220,27 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge receiveSMP :: M () receiveSMP = forever $ do - ((_, srv, _), _, _, _tType, ntfId, msgOrErr) <- atomically $ readTBQueue msgQ - let smpQueue = SMPQueueNtf srv ntfId - case msgOrErr of - Right (SMP.NMSG nmsgNonce encNMsgMeta) -> do - ntfTs <- liftIO getSystemTime - st <- asks store - NtfPushServer {pushQ} <- asks pushServer - stats <- asks serverStats - atomically $ updatePeriodStats (activeSubs stats) ntfId - atomically $ - findNtfSubscriptionToken st smpQueue - >>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta})) - incNtfStat ntfReceived - Right SMP.END -> updateSubStatus smpQueue NSEnd - Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e - Right _ -> logError $ "SMP server unexpected response" - Left e -> logError $ "SMP client error: " <> tshow e + ((_, srv, _), _, _, ts) <- atomically $ readTBQueue msgQ + forM ts $ \(ntfId, t) -> case t of + STUnexpectedError e -> logError $ "SMP client unexpected error: " <> tshow e -- uncorrelated response, should not happen + STResponse {} -> pure () -- it was already reported as timeout error + STEvent msgOrErr -> do + let smpQueue = SMPQueueNtf srv ntfId + case msgOrErr of + Right (SMP.NMSG nmsgNonce encNMsgMeta) -> do + ntfTs <- liftIO getSystemTime + st <- asks store + NtfPushServer {pushQ} <- asks pushServer + stats <- asks serverStats + atomically $ updatePeriodStats (activeSubs stats) ntfId + atomically $ + findNtfSubscriptionToken st smpQueue + >>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta})) + incNtfStat ntfReceived + Right SMP.END -> updateSubStatus smpQueue NSEnd + Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e + Right _ -> logError $ "SMP server unexpected response" + Left e -> logError $ "SMP client error: " <> tshow e receiveAgent = forever $ @@ -248,7 +252,6 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge forM_ subs $ \(_, ntfId) -> do let smpQueue = SMPQueueNtf srv ntfId updateSubStatus smpQueue NSInactive - CAResubscribed srv subs -> do forM_ subs $ \(_, ntfId) -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive logSubStatus srv "resubscribed" $ length subs diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 467700784..1c458a062 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -120,7 +120,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do -- send via proxy to unsecured queue Right () <- proxySMPMessage pc sess Nothing sndId noMsgFlags msg -- receive 1 - (_tSess, _v, _sid, _isResp, _entId, Right (SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody})) <- atomically $ readTBQueue msgQ + (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody})))]) <- atomically $ readTBQueue msgQ liftIO $ dec msgId encBody `shouldBe` Right msg ackSMPMessage rc rPriv rcvId msgId -- secure queue @@ -129,7 +129,7 @@ deliverMessageViaProxy proxyServ relayServ alg msg msg' = do -- send via proxy to secured queue Right () <- proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' -- receive 2 - (_tSess, _v, _sid, _isResp, _entId, Right (SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'})) <- atomically $ readTBQueue msgQ + (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'})))]) <- atomically $ readTBQueue msgQ liftIO $ dec msgId' encBody' `shouldBe` Right msg' ackSMPMessage rc rPriv rcvId msgId' From 2e5433676eaa5de93ed1ea9726706b9633308477 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 20 May 2024 17:14:04 +0400 Subject: [PATCH 048/125] xftp: check proxy before downloading from unknown server (#1102) * xftp: check proxy before downloading from unknown server * corrections * remove import * tests * comment * tests * don't wrap into internal error * fix tests --- simplexmq.cabal | 1 + src/Simplex/FileTransfer/Agent.hs | 35 ++++++----- src/Simplex/FileTransfer/Transport.hs | 4 ++ src/Simplex/Messaging/Agent.hs | 7 ++- src/Simplex/Messaging/Agent/Client.hs | 7 +++ src/Simplex/Messaging/Agent/Store/SQLite.hs | 61 ++++++++++--------- .../Agent/Store/SQLite/Migrations.hs | 4 +- .../M20240417_rcv_files_approved_relays.hs | 18 ++++++ .../Store/SQLite/Migrations/agent_schema.sql | 1 + src/Simplex/Messaging/Client.hs | 1 + tests/AgentTests/SQLiteTests.hs | 10 +-- tests/XFTPAgent.hs | 29 +++++---- 12 files changed, 110 insertions(+), 68 deletions(-) create mode 100644 src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs diff --git a/simplexmq.cabal b/simplexmq.cabal index d077deaae..7d2759b46 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -105,6 +105,7 @@ library Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem + Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays Simplex.Messaging.Agent.TRcvQueues Simplex.Messaging.Client Simplex.Messaging.Client.Agent diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index a5ba40f4a..c8030b206 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -112,8 +112,8 @@ closeXFTPAgent a = do where stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker) -xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AM RcvFileId -xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs = do +xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AM RcvFileId +xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs approvedRelays = do g <- asks random prefixPath <- lift $ getPrefixPath "rcv.xftp" createDirectory prefixPath @@ -124,7 +124,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi lift $ createEmptyFile =<< toFSFilePath relSavePath let saveFile = CryptoFile relSavePath cfArgs fId <- case redirect of - Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile + Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile approvedRelays Just _ -> do -- prepare description paths let relTmpPathRedirect = relPrefixPath "xftp.redirect-encrypted" @@ -134,7 +134,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi cfArgsRedirect <- atomically $ CF.randomArgs g let saveFileRedirect = CryptoFile relSavePathRedirect $ Just cfArgsRedirect -- create download tasks - withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile + withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile approvedRelays forM_ chunks (downloadChunk c) pure fId @@ -176,12 +176,12 @@ runXFTPRcvWorker c srv Worker {doWork} = do runXFTPOperation :: AgentConfig -> AM () runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case - RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []} -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) "chunk has no replicas" - fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do + (RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _) -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (INTERNAL "chunk has no replicas") + (fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays) -> do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do liftIO $ waitForUserNetwork c - downloadFileChunk fc replica + downloadFileChunk fc replica approvedRelays `catchAgentError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e where retryLoop loop e replicaDelay = do @@ -191,9 +191,10 @@ runXFTPRcvWorker c srv Worker {doWork} = do withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay atomically $ assertAgentForeground c loop - retryDone e = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (show e) - downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> AM () - downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do + retryDone = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) + downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM () + downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do + unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwError $ XFTP "" XFTP.NOT_APPROVED fsFileTmpPath <- lift $ toFSFilePath fileTmpPath chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest) @@ -214,6 +215,10 @@ runXFTPRcvWorker c srv Worker {doWork} = do when complete . lift . void $ getXFTPRcvWorker True c Nothing where + ipAddressProtected' :: AM Bool + ipAddressProtected' = do + cfg <- liftIO $ getNetworkConfig' c + pure $ ipAddressProtected cfg srv receivedSize :: [RcvFileChunk] -> Int64 receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0 receivedChunkSize ch@RcvFileChunk {chunkSize = s} @@ -234,11 +239,11 @@ retryOnError name loop done e = do then loop else done -rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> AM () -rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath internalErrStr = do +rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> AgentErrorType -> AM () +rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath err = do lift $ forM_ tmpPath (removePath <=< toFSFilePath) - withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr - notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr + withStore' c $ \db -> updateRcvFileError db rcvFileId (show err) + notify c rcvFileEntityId $ RFERR err runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM () runXFTPRcvLocalWorker c Worker {doWork} = do @@ -252,7 +257,7 @@ runXFTPRcvLocalWorker c Worker {doWork} = do runXFTPOperation AgentConfig {rcvFilesTTL} = withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $ \f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} -> - decryptFile f `catchAgentError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show) + decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath decryptFile :: RcvFile -> AM () decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do let CryptoFile savePath cfArgs = saveFile diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 244e00972..2f0a5de4f 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -217,6 +217,8 @@ data XFTPErrorType TIMEOUT | -- | bad redirect data REDIRECT {redirectError :: String} + | -- | cannot proceed with download from not approved relays without proxy + NOT_APPROVED | -- | internal server error INTERNAL | -- | used internally, never returned by the server (to be removed) @@ -249,6 +251,7 @@ instance Encoding XFTPErrorType where FILE_IO -> "FILE_IO" TIMEOUT -> "TIMEOUT" REDIRECT err -> "REDIRECT " <> smpEncode err + NOT_APPROVED -> "NOT_APPROVED" INTERNAL -> "INTERNAL" DUPLICATE_ -> "DUPLICATE_" @@ -268,6 +271,7 @@ instance Encoding XFTPErrorType where "FILE_IO" -> pure FILE_IO "TIMEOUT" -> pure TIMEOUT "REDIRECT" -> REDIRECT <$> _smpP + "NOT_APPROVED" -> pure NOT_APPROVED "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ _ -> fail "bad error type" diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3096291de..52d9b0086 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -83,6 +83,7 @@ module Simplex.Messaging.Agent setNtfServers, setNetworkConfig, getNetworkConfig, + getNetworkConfig', setUserNetworkInfo, reconnectAllServers, registerNtfToken, @@ -426,7 +427,7 @@ setNetworkConfig c@AgentClient {useNetworkConfig} cfg' = do -- returns fast network config getNetworkConfig :: AgentClient -> IO NetworkConfig -getNetworkConfig = fmap snd . readTVarIO . useNetworkConfig +getNetworkConfig = getNetworkConfig' {-# INLINE getNetworkConfig #-} setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO () @@ -483,8 +484,8 @@ xftpStartWorkers c = withAgentEnv c . startXFTPWorkers c {-# INLINE xftpStartWorkers #-} -- | Receive XFTP file -xftpReceiveFile :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AE RcvFileId -xftpReceiveFile c = withAgentEnv c .:. xftpReceiveFile' c +xftpReceiveFile :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AE RcvFileId +xftpReceiveFile c = withAgentEnv c .:: xftpReceiveFile' c {-# INLINE xftpReceiveFile #-} -- | Delete XFTP rcv file (deletes work files from file system and db records) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 64f8c172f..addd889a8 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -28,6 +28,7 @@ module Simplex.Messaging.Agent.Client withConnLocks, withInvLock, withLockMap, + ipAddressProtected, closeAgentClient, closeProtocolServerClients, reconnectServerClients, @@ -109,6 +110,7 @@ module Simplex.Messaging.Agent.Client waitUntilActive, UserNetworkInfo (..), UserNetworkType (..), + getNetworkConfig', waitForUserNetwork, isNetworkOnline, isOnline, @@ -854,6 +856,11 @@ getNetworkConfig c = do UNNone -> slowCfg _ -> fastCfg +-- returns fast network config +getNetworkConfig' :: AgentClient -> IO NetworkConfig +getNetworkConfig' = fmap snd . readTVarIO . useNetworkConfig +{-# INLINE getNetworkConfig' #-} + waitForUserNetwork :: AgentClient -> IO () waitForUserNetwork c = unlessM (atomically $ isNetworkOnline c) $ do diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index e47d2a15c..04dd826a6 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -2275,20 +2275,20 @@ getXFTPServerId_ db ProtocolServer {host, port, keyHash} = do firstRow fromOnly SEXFTPServerNotFound $ DB.query db "SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ?" (host, port, keyHash) -createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId) -createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file = runExceptT $ do - (rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing +createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId) +createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file approvedRelays = runExceptT $ do + (rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing approvedRelays liftIO $ forM_ chunks $ \fc@FileChunk {replicas} -> do chunkId <- insertRcvFileChunk db fc rcvFileId forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId pure rcvFileEntityId -createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId) -createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect" -createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile = runExceptT $ do - (dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing - (_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId) +createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId) +createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect" +createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile approvedRelays = runExceptT $ do + (dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing approvedRelays + (_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId) approvedRelays liftIO $ forM_ redirectChunks $ \fc@FileChunk {replicas} -> do chunkId <- insertRcvFileChunk db fc redirectId @@ -2308,8 +2308,8 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire chunks = [] } -insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> IO (Either StoreError (RcvFileId, DBRcvFileId)) -insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ = runExceptT $ do +insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> Bool -> IO (Either StoreError (RcvFileId, DBRcvFileId)) +insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ approvedRelays = runExceptT $ do let (redirectDigest_, redirectSize_) = case redirect of Just RedirectFileInfo {digest = d, size = s} -> (Just d, Just s) Nothing -> (Nothing, Nothing) @@ -2317,8 +2317,8 @@ insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSiz createWithRandomId gVar $ \rcvFileEntityId -> DB.execute db - "INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_)) + "INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_, approvedRelays)) rcvFileId <- liftIO $ insertedRowId db pure (rcvFileEntityId, rcvFileId) @@ -2468,7 +2468,7 @@ deleteRcvFile' :: DB.Connection -> DBRcvFileId -> IO () deleteRcvFile' db rcvFileId = DB.execute db "DELETE FROM rcv_files WHERE rcv_file_id = ?" (Only rcvFileId) -getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFileChunk)) +getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe (RcvFileChunk, Bool))) getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = do getWorkItem "rcv_file_download" getReplicaId getChunkData (markRcvFileFailed db . snd) where @@ -2492,7 +2492,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d LIMIT 1 |] (host, port, keyHash, RFSReceiving, cutoffTs) - getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError RcvFileChunk) + getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError (RcvFileChunk, Bool)) getChunkData (rcvFileChunkReplicaId, _fileId) = firstRow toChunk SEFileNotFound $ DB.query @@ -2500,7 +2500,8 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d [sql| SELECT f.rcv_file_id, f.rcv_file_entity_id, f.user_id, c.rcv_file_chunk_id, c.chunk_no, c.chunk_size, c.digest, f.tmp_path, c.tmp_path, - r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries + r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries, + f.approved_relays FROM rcv_file_chunk_replicas r JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id @@ -2509,20 +2510,22 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d |] (Only rcvFileChunkReplicaId) where - toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int)) -> RcvFileChunk - toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries)) = - RcvFileChunk - { rcvFileId, - rcvFileEntityId, - userId, - rcvChunkId, - chunkNo, - chunkSize, - digest, - fileTmpPath, - chunkTmpPath, - replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}] - } + toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int) :. Only Bool) -> (RcvFileChunk, Bool) + toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries) :. (Only approvedRelays)) = + ( RcvFileChunk + { rcvFileId, + rcvFileEntityId, + userId, + rcvChunkId, + chunkNo, + chunkSize, + digest, + fileTmpPath, + chunkTmpPath, + replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}] + }, + approvedRelays + ) getNextRcvFileToDecrypt :: DB.Connection -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFile)) getNextRcvFileToDecrypt db ttl = diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index 344a3f9ce..5a5ed5b5b 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -71,6 +71,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_deliver import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem +import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Transport.Client (TransportHost) @@ -110,7 +111,8 @@ schemaMigrations = ("m20240121_message_delivery_indexes", m20240121_message_delivery_indexes, Just down_m20240121_message_delivery_indexes), ("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect), ("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery), - ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem) + ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem), + ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs new file mode 100644 index 000000000..9eb10c27a --- /dev/null +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240417_rcv_files_approved_relays :: Query +m20240417_rcv_files_approved_relays = + [sql| +ALTER TABLE rcv_files ADD COLUMN approved_relays INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240417_rcv_files_approved_relays :: Query +down_m20240417_rcv_files_approved_relays = + [sql| +ALTER TABLE rcv_files DROP COLUMN approved_relays; +|] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index 0818be904..caf94418a 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -287,6 +287,7 @@ CREATE TABLE rcv_files( redirect_entity_id BLOB, redirect_size INTEGER, redirect_digest BLOB, + approved_relays INTEGER NOT NULL DEFAULT 0, UNIQUE(rcv_file_entity_id) ); CREATE TABLE rcv_file_chunks( diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 2e05812cb..34aa42462 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -84,6 +84,7 @@ module Simplex.Messaging.Client ServerTransmissionBatch, ServerTransmission (..), ClientCommand, + HostMode (..), -- * For testing PCTransmission, diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 436dd0eca..63466b9d7 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -709,15 +709,15 @@ testGetNextRcvChunkToDownload st = do withTransaction st $ \db -> do Right Nothing <- getNextRcvChunkToDownload db xftpServer1 86400 - Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1" - Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True Left e <- getNextRcvChunkToDownload db xftpServer1 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT rcv_file_id FROM rcv_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] - Right (Just RcvFileChunk {rcvFileEntityId}) <- getNextRcvChunkToDownload db xftpServer1 86400 + Right (Just (RcvFileChunk {rcvFileEntityId}, _)) <- getNextRcvChunkToDownload db xftpServer1 86400 rcvFileEntityId `shouldBe` fId2 testGetNextRcvFileToDecrypt :: SQLiteStore -> Expectation @@ -726,10 +726,10 @@ testGetNextRcvFileToDecrypt st = do withTransaction st $ \db -> do Right Nothing <- getNextRcvFileToDecrypt db 86400 - Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 1" DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1" - Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 2" Left e <- getNextRcvFileToDecrypt db 86400 diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 0610bf48d..8f42ec16a 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -179,7 +179,7 @@ testXFTPAgentSendReceiveRedirect = withXFTPServer $ do withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do FileDescriptionURI {description} <- either fail pure $ strDecode uri - rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing + rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing True rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 65536 totalSize) -- extra RFPROG before switching to real file rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize) rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 8388608 totalSize) @@ -223,7 +223,7 @@ testXFTPAgentSendReceiveNoRedirect = withXFTPServer $ do FileDescriptionURI {description} <- either fail pure $ strDecode uri let ValidFileDescription FileDescription {redirect} = description redirect `shouldBe` Nothing - rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing + rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing True -- NO extra "RFPROG 65k 65k" before switching to real file rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize) rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 5242880 totalSize) @@ -311,7 +311,7 @@ testReceive' rcp rfd originalFilePath = testReceiveCF' rcp rfd Nothing originalF testReceiveCF' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> Int64 -> ExceptT AgentErrorType IO RcvFileId testReceiveCF' rcp rfd cfArgs originalFilePath size = do - rfId <- xftpReceiveFile rcp 1 rfd cfArgs + rfId <- xftpReceiveFile rcp 1 rfd cfArgs True rfProgress rcp size ("", rfId', RFDONE path) <- rfGet rcp liftIO $ do @@ -336,7 +336,7 @@ testXFTPAgentReceiveRestore = do -- receive file - should not succeed with server down rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do xftpStartWorkers rcp (Just recipientFiles) - rfId <- xftpReceiveFile rcp 1 rfd Nothing + rfId <- xftpReceiveFile rcp 1 rfd Nothing True liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt pure rfId @@ -380,7 +380,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do -- receive file - should not succeed with server down rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do xftpStartWorkers rcp (Just recipientFiles) - rfId <- xftpReceiveFile rcp 1 rfd Nothing + rfId <- xftpReceiveFile rcp 1 rfd Nothing True liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt pure rfId @@ -392,8 +392,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do -- receive file - should fail with AUTH error withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do runRight_ $ xftpStartWorkers rcp' (Just recipientFiles) - ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- - rfGet rcp' + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp' rfId' `shouldBe` rfId -- tmp path should be removed after permanent error @@ -507,8 +506,8 @@ testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $ -- receive file - should fail with AUTH error withAgent 3 agentCfg initAgentServers testDB2 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) - rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp2 liftIO $ rfId' `shouldBe` rfId @@ -545,8 +544,8 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do -- receive file - should fail with AUTH error withAgent 5 agentCfg initAgentServers testDB3 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) - rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp2 liftIO $ rfId' `shouldBe` rfId @@ -580,8 +579,8 @@ testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $ runRight_ . void $ do -- receive file 1 again - rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing - ("", rfId1', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing True + ("", rfId1', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp liftIO $ rfId1 `shouldBe` rfId1' @@ -613,8 +612,8 @@ testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do -- receive file 1 again - should fail with AUTH error runRight $ do - rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing True + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp liftIO $ rfId' `shouldBe` rfId From 8fe18c4f6d1a5403e128592fab38af98cef40043 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Mon, 20 May 2024 17:48:21 +0300 Subject: [PATCH 049/125] core: use catMaybes to collect optional replies (#1161) * core: use catMaybes to collect optional replies * style * rollback agent changes and add a note --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Agent.hs | 2 +- src/Simplex/Messaging/Client.hs | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 52d9b0086..a08c5c99e 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -2093,7 +2093,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) SMP.SUB -> case respOrErr of Right SMP.OK -> processSubOk rq upConnIds Right msg@SMP.MSG {} -> do - processSubOk rq upConnIds + processSubOk rq upConnIds -- the connection is UP even when processing this particular message fails processSMP rq conn (toConnData conn) msg Right r -> processSubErr rq $ unexpectedResponse r Left e -> unless (temporaryClientError e) $ processSubErr rq e -- timeout/network was already reported diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 34aa42462..f98fac56b 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -113,7 +113,7 @@ import Data.Int (Int64) import Data.List (find) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime) import qualified Data.X509 as X import qualified Data.X509.Validation as XV @@ -508,14 +508,12 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize processMsgs :: ProtocolClient v err msg -> NonEmpty (SignedTransmission err msg) -> IO () processMsgs c ts = do - tsVar <- newTVarIO [] - mapM_ (processMsg c tsVar) ts - ts' <- readTVarIO tsVar + ts' <- catMaybes <$> mapM (processMsg c) (L.toList ts) forM_ msgQ $ \q -> mapM_ (atomically . writeTBQueue q . serverTransmission c) (L.nonEmpty ts') - processMsg :: ProtocolClient v err msg -> TVar [(EntityId, ServerTransmission err msg)] -> SignedTransmission err msg -> IO () - processMsg ProtocolClient {client_ = PClient {sentCommands}} tsVar (_, _, (corrId, entId, respOrErr)) + processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO (Maybe (EntityId, ServerTransmission err msg)) + processMsg ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr)) | B.null $ bs corrId = sendMsg $ STEvent clientResp | otherwise = atomically (TM.lookup corrId sentCommands) >>= \case @@ -528,7 +526,9 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize (swapTVar pending False) (True <$ tryPutTMVar responseVar (if entityId == entId then clientResp else Left unexpected)) (pure False) - unless wasPending $ sendMsg $ if entityId == entId then STResponse command clientResp else STUnexpectedError unexpected + if wasPending + then pure Nothing + else sendMsg $ if entityId == entId then STResponse command clientResp else STUnexpectedError unexpected where unexpected = unexpectedResponse respOrErr clientResp = case respOrErr of @@ -536,12 +536,13 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize Right r -> case protocolError r of Just e -> Left $ PCEProtocolError e _ -> Right r - sendMsg :: ServerTransmission err msg -> IO () + sendMsg :: ServerTransmission err msg -> IO (Maybe (EntityId, ServerTransmission err msg)) sendMsg t = case msgQ of - Just _ -> atomically $ modifyTVar' tsVar ((entId, t) :) - Nothing -> case clientResp of - Left e -> logError ("SMP client error: " <> tshow e) - Right _ -> logWarn ("SMP client unprocessed event") + Just _ -> pure $ Just (entId, t) + Nothing -> + Nothing <$ case clientResp of + Left e -> logError $ "SMP client error: " <> tshow e + Right _ -> logWarn "SMP client unprocessed event" unexpectedResponse :: Show r => r -> ProtocolClientError err unexpectedResponse = PCEUnexpectedResponse . B.pack . take 32 . show From f89d715a995312d76dbe8b6d74667a86fd12da58 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Mon, 20 May 2024 19:07:33 +0300 Subject: [PATCH 050/125] smp server: add proxy stats (#1157) * smp-server: add proxy counters * count simplex.im messages * update * fix * get own servers from INI * remove export --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Client.hs | 1 - src/Simplex/Messaging/Client/Agent.hs | 41 +++++-- src/Simplex/Messaging/Server.hs | 144 ++++++++++++++-------- src/Simplex/Messaging/Server/Env/STM.hs | 2 +- src/Simplex/Messaging/Server/Main.hs | 6 + src/Simplex/Messaging/Server/Stats.hs | 155 +++++++++++++++++++++--- tests/ServerTests.hs | 6 +- 7 files changed, 271 insertions(+), 84 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index f98fac56b..ecf4ee766 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -84,7 +84,6 @@ module Simplex.Messaging.Client ServerTransmissionBatch, ServerTransmission (..), ClientCommand, - HostMode (..), -- * For testing PCTransmission, diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 10b83157f..a7732b4d4 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -54,7 +54,7 @@ import UnliftIO.Exception (Exception) import qualified UnliftIO.Exception as E import UnliftIO.STM -type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) SMPClient) +type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) (OwnServer, SMPClient)) data SMPClientAgentEvent = CAConnected SMPServer @@ -75,7 +75,8 @@ data SMPClientAgentConfig = SMPClientAgentConfig persistErrorInterval :: NominalDiffTime, msgQSize :: Natural, agentQSize :: Natural, - agentSubsBatchSize :: Int + agentSubsBatchSize :: Int, + ownServerDomains :: [ByteString] } defaultSMPClientAgentConfig :: SMPClientAgentConfig @@ -91,7 +92,8 @@ defaultSMPClientAgentConfig = persistErrorInterval = 0, msgQSize = 256, agentQSize = 256, - agentSubsBatchSize = 900 + agentSubsBatchSize = 900, + ownServerDomains = [] } where second = 1000000 @@ -103,13 +105,15 @@ data SMPClientAgent = SMPClientAgent agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, smpClients :: TMap SMPServer SMPClientVar, - smpSessions :: TMap SessionId SMPClient, + smpSessions :: TMap SessionId (OwnServer, SMPClient), srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), smpSubWorkers :: TMap SMPServer (SessionVar (Async ())), workerSeq :: TVar Int } +type OwnServer = Bool + newtype InternalException e = InternalException {unInternalException :: e} deriving (Eq, Show) @@ -163,13 +167,17 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg -- | Get or create SMP client for SMPServer getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient -getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv = +getSMPServerClient' ca srv = snd <$> getSMPServerClient'' ca srv +{-# INLINE getSMPServerClient' #-} + +getSMPServerClient'' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO (OwnServer, SMPClient) +getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv = atomically getClientVar >>= either (ExceptT . newSMPClient) waitForSMPClient where getClientVar :: STM (Either SMPClientVar SMPClientVar) getClientVar = getSessVar workerSeq srv smpClients - waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient + waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO (OwnServer, SMPClient) waitForSMPClient v = do let ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) @@ -179,20 +187,22 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worker Just (Left (e, Just ts)) -> ifM ((ts <) <$> liftIO getCurrentTime) - (atomically (removeSessVar v srv smpClients) >> getSMPServerClient' ca srv) + (atomically (removeSessVar v srv smpClients) >> getSMPServerClient'' ca srv) (throwE e) Nothing -> throwE PCEResponseTimeout - newSMPClient :: SMPClientVar -> IO (Either SMPClientError SMPClient) + newSMPClient :: SMPClientVar -> IO (Either SMPClientError (OwnServer, SMPClient)) newSMPClient v = do r <- connectClient ca srv v `E.catch` (pure . Left . PCEIOError) case r of Right smp -> do logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv + let c = (isOwnServer ca srv, smp) atomically $ do - putTMVar (sessionVar v) (Right smp) - TM.insert (sessionId $ thParams smp) smp smpSessions + putTMVar (sessionVar v) (Right c) + TM.insert (sessionId $ thParams smp) c smpSessions notify ca $ CAConnected srv + pure $ Right c Left e -> do if persistErrorInterval agentCfg == 0 || e == PCENetworkError || e == PCEResponseTimeout then atomically $ do @@ -202,7 +212,12 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worker ts <- addUTCTime (persistErrorInterval agentCfg) <$> liftIO getCurrentTime atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) reconnectClient ca srv - pure r + pure $ Left e + +isOwnServer :: SMPClientAgent -> SMPServer -> OwnServer +isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} = + let srv = strEncode $ L.head host + in any (\s -> s == srv || (B.cons '.' s) `B.isSuffixOf` srv) (ownServerDomains agentCfg) -- | Run an SMP client for SMPClientVar connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient) @@ -298,7 +313,7 @@ notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m () notify ca evt = atomically $ writeTBQueue (agentQ ca) evt {-# INLINE notify #-} -lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe SMPClient) +lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe (OwnServer, SMPClient)) lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions closeSMPClientAgent :: SMPClientAgent -> IO () @@ -315,7 +330,7 @@ closeSMPServerClients c = atomically (smpClients c `swapTVar` M.empty) >>= mapM_ where closeClient v = atomically (readTMVar $ sessionVar v) >>= \case - Right smp -> closeProtocolClient smp `catchAll_` pure () + Right (_, smp) -> closeProtocolClient smp `catchAll_` pure () _ -> pure () cancelActions :: Foldable f => TVar (f (Async ())) -> IO () diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 6c8a18aa5..6cbc4d4aa 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -6,6 +6,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} @@ -68,8 +69,8 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError) -import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient', lookupSMPServerClient) +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError, temporaryClientError) +import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -232,7 +233,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath liftIO $ threadDelay' $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0) - ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount} <- asks serverStats + ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv} <- asks serverStats let interval = 1000000 * logInterval forever $ do withFile statsFilePath AppendMode $ \h -> liftIO $ do @@ -251,32 +252,46 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do msgSentNtf' <- atomically $ swapTVar msgSentNtf 0 msgRecvNtf' <- atomically $ swapTVar msgRecvNtf 0 psNtf <- atomically $ periodStatCounts activeQueuesNtf ts + pRelays' <- atomically $ getResetProxyStatsData pRelays + pRelaysOwn' <- atomically $ getResetProxyStatsData pRelaysOwn + pMsgFwds' <- atomically $ getResetProxyStatsData pMsgFwds + pMsgFwdsOwn' <- atomically $ getResetProxyStatsData pMsgFwdsOwn + pMsgFwdsRecv' <- atomically $ swapTVar pMsgFwdsRecv 0 qCount' <- readTVarIO qCount msgCount' <- readTVarIO msgCount hPutStrLn h $ intercalate "," - [ iso8601Show $ utctDay fromTime', - show qCreated', - show qSecured', - show qDeletedAll', - show msgSent', - show msgRecv', - dayCount ps, - weekCount ps, - monthCount ps, - show msgSentNtf', - show msgRecvNtf', - dayCount psNtf, - weekCount psNtf, - monthCount psNtf, - show qCount', - show msgCount', - show msgExpired', - show qDeletedNew', - show qDeletedSecured' - ] + ( [ iso8601Show $ utctDay fromTime', + show qCreated', + show qSecured', + show qDeletedAll', + show msgSent', + show msgRecv', + dayCount ps, + weekCount ps, + monthCount ps, + show msgSentNtf', + show msgRecvNtf', + dayCount psNtf, + weekCount psNtf, + monthCount psNtf, + show qCount', + show msgCount', + show msgExpired', + show qDeletedNew', + show qDeletedSecured' + ] + <> showProxyStats pRelays' + <> showProxyStats pRelaysOwn' + <> showProxyStats pMsgFwds' + <> showProxyStats pMsgFwdsOwn' + <> [show pMsgFwdsRecv'] + ) liftIO $ threadDelay' interval + where + showProxyStats ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} = + [show _pRequests, show _pSuccesses, show _pErrorsConnect, show _pErrorsCompat, show _pErrorsOther] runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M () runClient signKey tp h = do @@ -346,7 +361,13 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do subscriptions' <- bshow . M.size <$> readTVarIO subscriptions hPutStrLn h . B.unpack $ B.intercalate "," [bshow cid, encode sessionId, connected', strEncode createdAt, rcvActiveAt', sndActiveAt', bshow age, subscriptions'] CPStats -> withAdminRole $ do - ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgSentNtf, msgRecvNtf, qCount, msgCount} <- unliftIO u $ asks serverStats + ss <- unliftIO u $ asks serverStats + let putStat :: Show a => ByteString -> (ServerStats -> TVar a) -> IO () + putStat label var = readTVarIO (var ss) >>= \v -> B.hPutStr h $ label <> ": " <> bshow v <> "\n" + putProxyStat :: ByteString -> (ServerStats -> ProxyStats) -> IO () + putProxyStat label var = do + ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} <- atomically $ getProxyStatsData $ var ss + B.hPutStr h $ label <> ": requests=" <> bshow _pRequests <> ", successes=" <> bshow _pSuccesses <> ", errorsConnect=" <> bshow _pErrorsConnect <> ", errorsCompat=" <> bshow _pErrorsCompat <> ", errorsOther=" <> bshow _pErrorsOther <> "\n" putStat "fromTime" fromTime putStat "qCreated" qCreated putStat "qSecured" qSecured @@ -359,9 +380,11 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do putStat "msgRecvNtf" msgRecvNtf putStat "qCount" qCount putStat "msgCount" msgCount - where - putStat :: Show a => String -> TVar a -> IO () - putStat label var = readTVarIO var >>= \v -> hPutStrLn h $ label <> ": " <> show v + putProxyStat "pRelays" pRelays + putProxyStat "pRelaysOwn" pRelaysOwn + putProxyStat "pMsgFwds" pMsgFwds + putProxyStat "pMsgFwdsOwn" pMsgFwdsOwn + putStat "pMsgFwdsRecv" pMsgFwdsRecv CPStatsRTS -> getRTSStats >>= hPrint h CPThreads -> withAdminRole $ do #if MIN_VERSION_base(4,18,0) @@ -647,33 +670,56 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth getRelay = do - ProxyAgent {smpAgent} <- asks proxyAgent - liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) `catch` (pure . Left . PCEIOError) + ServerStats {pRelays, pRelaysOwn} <- asks serverStats + let inc = mkIncProxyStats pRelays pRelaysOwn + ProxyAgent {smpAgent = a} <- asks proxyAgent + liftIO (runExceptT (getSMPServerClient'' a srv) `catch` (pure . Left . PCEIOError)) >>= \case + Right (own, smp) -> do + inc own pRequests + case proxyResp smp of + r@PKEY {} -> r <$ inc own pSuccesses + r -> r <$ inc own pErrorsCompat + Left e -> do + let own = isOwnServer a srv + inc own pRequests + inc own $ if temporaryClientError e then pErrorsConnect else pErrorsOther + pure . ERR $ smpProxyError e where - proxyResp = \case - Left err -> ERR $ smpProxyError err - Right smp -> - let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp - in case compatibleVRange thServerVRange proxiedSMPRelayVRange of - -- Cap the destination relay version range to prevent client version fingerprinting. - -- See comment for proxiedSMPRelayVersion. - Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of - Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey - Nothing -> ERR $ transportErr TENoServerAuth - _ -> ERR $ transportErr TEVersion + proxyResp smp = + let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp + in case compatibleVRange thServerVRange proxiedSMPRelayVRange of + -- Cap the destination relay version range to prevent client version fingerprinting. + -- See comment for proxiedSMPRelayVersion. + Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of + Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey + Nothing -> ERR $ transportErr TENoServerAuth + _ -> ERR $ transportErr TEVersion PFWD fwdV pubKey encBlock -> do - ProxyAgent {smpAgent} <- asks proxyAgent - atomically (lookupSMPServerClient smpAgent sessId) >>= \case - Just smp - | v >= sendingProxySMPVersion -> - liftIO $ either (ERR . smpProxyError) PRES <$> - runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catchError` (pure . Left . PCEIOError) - | otherwise -> pure . ERR $ transportErr TEVersion + ProxyAgent {smpAgent = a} <- asks proxyAgent + ServerStats {pMsgFwds, pMsgFwdsOwn} <- asks serverStats + let inc = mkIncProxyStats pMsgFwds pMsgFwdsOwn + atomically (lookupSMPServerClient a sessId) >>= \case + Just (own, smp) -> do + inc own pRequests + if + | v >= sendingProxySMPVersion -> + liftIO (runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case + Right r -> PRES r <$ inc own pSuccesses + Left e -> case e of + PCEProtocolError {} -> ERR err <$ inc own pSuccesses + _ -> ERR err <$ inc own pErrorsOther + where + err = smpProxyError e + | otherwise -> ERR (transportErr TEVersion) <$ inc own pErrorsCompat where THandleParams {thVersion = v} = thParams smp - Nothing -> pure $ ERR $ PROXY NO_SESSION + Nothing -> inc False pRequests >> inc False pErrorsConnect $> ERR (PROXY NO_SESSION) transportErr :: TransportError -> ErrorType transportErr = PROXY . BROKER . TRANSPORT + mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> TVar Int) -> m () + mkIncProxyStats ps psOwn = \own sel -> do + atomically $ modifyTVar' (sel ps) (+ 1) + when own $ atomically $ modifyTVar' (sel psOwn) (+ 1) processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = do st <- asks queueStore @@ -987,6 +1033,8 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) + stats <- asks serverStats + atomically $ modifyTVar' (pMsgFwdsRecv stats) (+ 1) pure $ RRES r3 where rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index f23192aeb..52a6094bc 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -126,7 +126,7 @@ data Server = Server savingLock :: Lock } -data ProxyAgent = ProxyAgent +newtype ProxyAgent = ProxyAgent { smpAgent :: SMPClientAgent } diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index fa1698653..6af1ce2a5 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -10,6 +10,7 @@ module Simplex.Messaging.Server.Main where import Control.Concurrent.STM import Control.Monad (void) +import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) @@ -147,6 +148,8 @@ smpServerCLI cfgPath logPath = \# It defines prefferred hostname for destination servers with multiple hostnames.\n\ \# host_mode: public\n\ \# required_host_mode: off\n\n\ + \# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics.\n\ + \# own_server_domains: \n\n\ \# SOCKS proxy port for forwarding messages to destination servers.\n\ \# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\ \# socks_proxy: localhost:9050\n\n\ @@ -245,6 +248,7 @@ smpServerCLI cfgPath logPath = requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini } }, + ownServerDomains = either (const []) textToOwnServers $ lookupValue "PROXY" "own_server_domains" ini, persistErrorInterval = 30 -- seconds }, allowSMPProxy = True @@ -259,6 +263,8 @@ smpServerCLI cfgPath logPath = "public" -> HMPublic "onion" -> HMOnionViaSocks s -> error . T.unpack $ "Invalid host_mode: " <> s + textToOwnServers :: Text -> [ByteString] + textToOwnServers = map encodeUtf8 . T.words data CliCommand = Init InitOptions diff --git a/src/Simplex/Messaging/Server/Stats.hs b/src/Simplex/Messaging/Server/Stats.hs index 0b4c677c2..d8935b44b 100644 --- a/src/Simplex/Messaging/Server/Stats.hs +++ b/src/Simplex/Messaging/Server/Stats.hs @@ -33,6 +33,11 @@ data ServerStats = ServerStats msgSentNtf :: TVar Int, msgRecvNtf :: TVar Int, activeQueuesNtf :: PeriodStats RecipientId, + pRelays :: ProxyStats, + pRelaysOwn :: ProxyStats, + pMsgFwds :: ProxyStats, + pMsgFwdsOwn :: ProxyStats, + pMsgFwdsRecv :: TVar Int, qCount :: TVar Int, msgCount :: TVar Int } @@ -51,6 +56,11 @@ data ServerStatsData = ServerStatsData _msgSentNtf :: Int, _msgRecvNtf :: Int, _activeQueuesNtf :: PeriodStatsData RecipientId, + _pRelays :: ProxyStatsData, + _pRelaysOwn :: ProxyStatsData, + _pMsgFwds :: ProxyStatsData, + _pMsgFwdsOwn :: ProxyStatsData, + _pMsgFwdsRecv :: Int, _qCount :: Int, _msgCount :: Int } @@ -71,9 +81,14 @@ newServerStats ts = do msgSentNtf <- newTVar 0 msgRecvNtf <- newTVar 0 activeQueuesNtf <- newPeriodStats + pRelays <- newProxyStats + pRelaysOwn <- newProxyStats + pMsgFwds <- newProxyStats + pMsgFwdsOwn <- newProxyStats + pMsgFwdsRecv <- newTVar 0 qCount <- newTVar 0 msgCount <- newTVar 0 - pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount} + pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv, qCount, msgCount} getServerStatsData :: ServerStats -> STM ServerStatsData getServerStatsData s = do @@ -90,9 +105,14 @@ getServerStatsData s = do _msgSentNtf <- readTVar $ msgSentNtf s _msgRecvNtf <- readTVar $ msgRecvNtf s _activeQueuesNtf <- getPeriodStatsData $ activeQueuesNtf s + _pRelays <- getProxyStatsData $ pRelays s + _pRelaysOwn <- getProxyStatsData $ pRelaysOwn s + _pMsgFwds <- getProxyStatsData $ pMsgFwds s + _pMsgFwdsOwn <- getProxyStatsData $ pMsgFwdsOwn s + _pMsgFwdsRecv <- readTVar $ pMsgFwdsRecv s _qCount <- readTVar $ qCount s _msgCount <- readTVar $ msgCount s - pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _qCount, _msgCount} + pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount} setServerStats :: ServerStats -> ServerStatsData -> STM () setServerStats s d = do @@ -109,28 +129,42 @@ setServerStats s d = do writeTVar (msgSentNtf s) $! _msgSentNtf d writeTVar (msgRecvNtf s) $! _msgRecvNtf d setPeriodStats (activeQueuesNtf s) (_activeQueuesNtf d) + setProxyStats (pRelays s) $! _pRelays d + setProxyStats (pRelaysOwn s) $! _pRelaysOwn d + setProxyStats (pMsgFwds s) $! _pMsgFwds d + setProxyStats (pMsgFwdsOwn s) $! _pMsgFwdsOwn d + writeTVar (pMsgFwdsRecv s) $! _pMsgFwdsRecv d writeTVar (qCount s) $! _qCount d writeTVar (msgCount s) $! _msgCount d instance StrEncoding ServerStatsData where - strEncode ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount} = + strEncode d = B.unlines - [ "fromTime=" <> strEncode _fromTime, - "qCreated=" <> strEncode _qCreated, - "qSecured=" <> strEncode _qSecured, - "qDeletedAll=" <> strEncode _qDeletedAll, - "qDeletedNew=" <> strEncode _qDeletedNew, - "qDeletedSecured=" <> strEncode _qDeletedSecured, - "qCount=" <> strEncode _qCount, - "msgSent=" <> strEncode _msgSent, - "msgRecv=" <> strEncode _msgRecv, - "msgExpired=" <> strEncode _msgExpired, - "msgSentNtf=" <> strEncode _msgSentNtf, - "msgRecvNtf=" <> strEncode _msgRecvNtf, + [ "fromTime=" <> strEncode (_fromTime d), + "qCreated=" <> strEncode (_qCreated d), + "qSecured=" <> strEncode (_qSecured d), + "qDeletedAll=" <> strEncode (_qDeletedAll d), + "qDeletedNew=" <> strEncode (_qDeletedNew d), + "qDeletedSecured=" <> strEncode (_qDeletedSecured d), + "qCount=" <> strEncode (_qCount d), + "msgSent=" <> strEncode (_msgSent d), + "msgRecv=" <> strEncode (_msgRecv d), + "msgExpired=" <> strEncode (_msgExpired d), + "msgSentNtf=" <> strEncode (_msgSentNtf d), + "msgRecvNtf=" <> strEncode (_msgRecvNtf d), "activeQueues:", - strEncode _activeQueues, + strEncode (_activeQueues d), "activeQueuesNtf:", - strEncode _activeQueuesNtf + strEncode (_activeQueuesNtf d), + "pRelays:", + strEncode (_pRelays d), + "pRelaysOwn:", + strEncode (_pRelaysOwn d), + "pMsgFwds:", + strEncode (_pMsgFwds d), + "pMsgFwdsOwn:", + strEncode (_pMsgFwdsOwn d), + "pMsgFwdsRecv=" <> strEncode (_pMsgFwdsRecv d) ] strP = do _fromTime <- "fromTime=" *> strP <* A.endOfLine @@ -157,7 +191,17 @@ instance StrEncoding ServerStatsData where optional ("activeQueuesNtf:" <* A.endOfLine) >>= \case Just _ -> strP <* optional A.endOfLine _ -> pure newPeriodStatsData - pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount = 0} + _pRelays <- proxyStatsP "pRelays:" + _pRelaysOwn <- proxyStatsP "pRelaysOwn:" + _pMsgFwds <- proxyStatsP "pMsgFwds:" + _pMsgFwdsOwn <- proxyStatsP "pMsgFwdsOwn:" + _pMsgFwdsRecv <- "pMsgFwdsRecv=" *> strP <* A.endOfLine <|> pure 0 + pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount = 0} + where + proxyStatsP key = + optional (A.string key >> A.endOfLine) >>= \case + Just _ -> strP <* optional A.endOfLine + _ -> pure newProxyStatsData data PeriodStats a = PeriodStats { day :: TVar (Set a), @@ -231,3 +275,78 @@ updatePeriodStats stats pId = do updatePeriod month where updatePeriod pSel = modifyTVar' (pSel stats) (S.insert pId) + +data ProxyStats = ProxyStats + { pRequests :: TVar Int, + pSuccesses :: TVar Int, -- includes destination server error responses that will be forwarded to the client + pErrorsConnect :: TVar Int, + pErrorsCompat :: TVar Int, + pErrorsOther :: TVar Int + } + +newProxyStats :: STM ProxyStats +newProxyStats = do + pRequests <- newTVar 0 + pSuccesses <- newTVar 0 + pErrorsConnect <- newTVar 0 + pErrorsCompat <- newTVar 0 + pErrorsOther <- newTVar 0 + pure ProxyStats {pRequests, pSuccesses, pErrorsConnect, pErrorsCompat, pErrorsOther} + +data ProxyStatsData = ProxyStatsData + { _pRequests :: Int, + _pSuccesses :: Int, + _pErrorsConnect :: Int, + _pErrorsCompat :: Int, + _pErrorsOther :: Int + } + deriving (Show) + +newProxyStatsData :: ProxyStatsData +newProxyStatsData = ProxyStatsData {_pRequests = 0, _pSuccesses = 0, _pErrorsConnect = 0, _pErrorsCompat = 0, _pErrorsOther = 0} + +getProxyStatsData :: ProxyStats -> STM ProxyStatsData +getProxyStatsData s = do + _pRequests <- readTVar $ pRequests s + _pSuccesses <- readTVar $ pSuccesses s + _pErrorsConnect <- readTVar $ pErrorsConnect s + _pErrorsCompat <- readTVar $ pErrorsCompat s + _pErrorsOther <- readTVar $ pErrorsOther s + pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} + +getResetProxyStatsData :: ProxyStats -> STM ProxyStatsData +getResetProxyStatsData s = do + _pRequests <- swapTVar (pRequests s) 0 + _pSuccesses <- swapTVar (pSuccesses s) 0 + _pErrorsConnect <- swapTVar (pErrorsConnect s) 0 + _pErrorsCompat <- swapTVar (pErrorsCompat s) 0 + _pErrorsOther <- swapTVar (pErrorsOther s) 0 + pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} + +setProxyStats :: ProxyStats -> ProxyStatsData -> STM () +setProxyStats s d = do + writeTVar (pRequests s) $! _pRequests d + writeTVar (pSuccesses s) $! _pSuccesses d + writeTVar (pErrorsConnect s) $! _pErrorsConnect d + writeTVar (pErrorsCompat s) $! _pErrorsCompat d + writeTVar (pErrorsOther s) $! _pErrorsOther d + +instance StrEncoding ProxyStatsData where + strEncode ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} = + "requests=" + <> strEncode _pRequests + <> "\nsuccesses=" + <> strEncode _pSuccesses + <> "\nerrorsConnect=" + <> strEncode _pErrorsConnect + <> "\nerrorsCompat=" + <> strEncode _pErrorsCompat + <> "\nerrorsOther=" + <> strEncode _pErrorsOther + strP = do + _pRequests <- "requests=" *> strP <* A.endOfLine + _pSuccesses <- "successes=" *> strP <* A.endOfLine + _pErrorsConnect <- "errorsConnect=" *> strP <* A.endOfLine + _pErrorsCompat <- "errorsCompat=" *> strP <* A.endOfLine + _pErrorsOther <- "errorsOther=" *> strP + pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index b0ed67913..b0c90ca96 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -608,7 +608,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 2 logSize testStoreMsgsFile `shouldReturn` 5 - logSize testServerStatsBackupFile `shouldReturn` 20 + logSize testServerStatsBackupFile `shouldReturn` 45 Right stats1 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats1 [rId] 5 1 @@ -626,7 +626,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 -- the last message is not removed because it was not ACK'd logSize testStoreMsgsFile `shouldReturn` 3 - logSize testServerStatsBackupFile `shouldReturn` 20 + logSize testServerStatsBackupFile `shouldReturn` 45 Right stats2 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats2 [rId] 5 3 @@ -645,7 +645,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 logSize testStoreMsgsFile `shouldReturn` 0 - logSize testServerStatsBackupFile `shouldReturn` 20 + logSize testServerStatsBackupFile `shouldReturn` 45 Right stats3 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats3 [rId] 5 5 From e3f5d244c1a435593e33adc023bf1f920f379f8d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 20 May 2024 17:31:08 +0100 Subject: [PATCH 051/125] 5.8.0.2 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 6b90c709f..7667d41ef 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.1 +version: 5.8.0.2 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 7d2759b46..d8cc1b54d 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.1 +version: 5.8.0.2 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From f50fa5c60b60c18044bc6c17e973f0abcfe77cac Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Tue, 21 May 2024 14:53:33 +0300 Subject: [PATCH 052/125] smp-server: limit concurrency in proxy command processing (#1162) * smp: put client proxy command processing threads under a shared semaphore * add LIMITS.max_proc_threads to server config * rename to PROXY.client_concurrency * retry on strictly greater than max concurrency * set default to 16 * rename * fix limit --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Server.hs | 41 +++++++++++++++++-------- src/Simplex/Messaging/Server/Env/STM.hs | 10 ++++-- src/Simplex/Messaging/Server/Main.hs | 9 ++++-- tests/SMPClient.hs | 3 +- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 6cbc4d4aa..c0ab7df8e 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -45,6 +45,7 @@ import Control.Monad.IO.Unlift import Control.Monad.Reader import Control.Monad.Trans.Except import Crypto.Random +import Control.Monad.STM (retry) import Data.Bifunctor (first) import Data.ByteString.Base64 (encode) import Data.ByteString.Char8 (ByteString) @@ -95,7 +96,6 @@ import System.Exit (exitFailure) import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode) import System.Mem.Weak (deRefWeak) import UnliftIO (timeout) -import UnliftIO.Async (mapConcurrently) import UnliftIO.Concurrent import UnliftIO.Directory (doesFileExist, renameFile) import UnliftIO.Exception @@ -182,12 +182,8 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do TM.lookupInsert qId clnt (subs s) $>>= clientToBeNotified endPreviousSubscriptions :: (QueueId, Client) -> M (Maybe s) endPreviousSubscriptions (qId, c) = do - tId <- atomically $ stateTVar (endThreadSeq c) $ \next -> (next, next + 1) - t <- forkIO $ do - labelMyThread $ label <> ".endPreviousSubscriptions" + forkClient c (label <> ".endPreviousSubscriptions") $ atomically $ writeTBQueue (sndQ c) [(CorrId "", qId, END)] - atomically $ modifyTVar' (endThreads c) $ IM.delete tId - mkWeakThreadId t >>= atomically . modifyTVar' (endThreads c) . IM.insert tId atomically $ TM.lookupDelete qId (clientSubs c) receiveFromProxyAgent :: ProxyAgent -> M () @@ -364,10 +360,10 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do ss <- unliftIO u $ asks serverStats let putStat :: Show a => ByteString -> (ServerStats -> TVar a) -> IO () putStat label var = readTVarIO (var ss) >>= \v -> B.hPutStr h $ label <> ": " <> bshow v <> "\n" - putProxyStat :: ByteString -> (ServerStats -> ProxyStats) -> IO () + putProxyStat :: ByteString -> (ServerStats -> ProxyStats) -> IO () putProxyStat label var = do ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} <- atomically $ getProxyStatsData $ var ss - B.hPutStr h $ label <> ": requests=" <> bshow _pRequests <> ", successes=" <> bshow _pSuccesses <> ", errorsConnect=" <> bshow _pErrorsConnect <> ", errorsCompat=" <> bshow _pErrorsCompat <> ", errorsOther=" <> bshow _pErrorsOther <> "\n" + B.hPutStr h $ label <> ": requests=" <> bshow _pRequests <> ", successes=" <> bshow _pSuccesses <> ", errorsConnect=" <> bshow _pErrorsConnect <> ", errorsCompat=" <> bshow _pErrorsCompat <> ", errorsOther=" <> bshow _pErrorsOther <> "\n" putStat "fromTime" fromTime putStat "qCreated" qCreated putStat "qSecured" qSecured @@ -650,18 +646,39 @@ dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/Xo dummyKeyX25519 :: C.PublicKey 'C.X25519 dummyKeyX25519 = "MCowBQYDK2VuAyEA4JGSMYht18H4mas/jHeBwfcM7jLwNYJNOAhi2/g4RXg=" +forkClient :: Client -> String -> M () -> M () +forkClient Client {endThreads, endThreadSeq} label action = do + tId <- atomically $ stateTVar endThreadSeq $ \next -> (next, next + 1) + t <- forkIO $ do + labelMyThread label + action `finally` atomically (modifyTVar' endThreads $ IM.delete tId) + mkWeakThreadId t >>= atomically . modifyTVar' endThreads . IM.insert tId + client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M () -client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do +client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId, procThreads} Server {subscribedQ, ntfSubscribedQ, notifiers} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands" forever $ do (proxied, rs) <- partitionEithers . L.toList <$> (mapM processCommand =<< atomically (readTBQueue rcvQ)) forM_ (L.nonEmpty rs) reply - -- TODO cancel this thread if the client gets disconnected - -- TODO limit client concurrency - forM_ (L.nonEmpty proxied) $ \cmds -> forkIO $ mapConcurrently processProxiedCmd cmds >>= reply + forM_ (L.nonEmpty proxied) $ \cmds -> mapM forkProxiedCmd cmds >>= mapM (atomically . takeTMVar) >>= reply where reply :: MonadIO m => NonEmpty (Transmission BrokerMsg) -> m () reply = atomically . writeTBQueue sndQ + forkProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (TMVar (Transmission BrokerMsg)) + forkProxiedCmd cmd = do + res <- newEmptyTMVarIO + bracket_ wait signal . forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " proxy") $ + -- commands MUST be processed under a reasonable timeout or the client would halt + processProxiedCmd cmd >>= atomically . putTMVar res + pure res + where + wait = do + ServerConfig {serverClientConcurrency} <- asks config + atomically $ do + used <- readTVar procThreads + when (used >= serverClientConcurrency) retry + writeTVar procThreads $! used + 1 + signal = atomically $ modifyTVar' procThreads (\t -> t - 1) processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR $ PROXY BASIC_AUTH) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 52a6094bc..77adb94f4 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -82,7 +82,8 @@ data ServerConfig = ServerConfig -- | run listener on control port controlPort :: Maybe ServiceName, smpAgentCfg :: SMPClientAgentConfig, - allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth` + allowSMPProxy :: Bool, -- auth is the same with `newQueueBasicAuth` + serverClientConcurrency :: Int } defMsgExpirationDays :: Int64 @@ -102,6 +103,9 @@ defaultInactiveClientExpiration = checkInterval = 3600 -- seconds, 1 hours } +defaultProxyClientConcurrency :: Int +defaultProxyClientConcurrency = 16 + data Env = Env { config :: ServerConfig, server :: Server, @@ -139,6 +143,7 @@ data Client = Client rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)), sndQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), msgQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), + procThreads :: TVar Int, endThreads :: TVar (IntMap (Weak ThreadId)), endThreadSeq :: TVar Int, thVersion :: VersionSMP, @@ -173,12 +178,13 @@ newClient nextClientId qSize thVersion sessionId createdAt = do rcvQ <- newTBQueue qSize sndQ <- newTBQueue qSize msgQ <- newTBQueue qSize + procThreads <- newTVar 0 endThreads <- newTVar IM.empty endThreadSeq <- newTVar 0 connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, procThreads, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 6af1ce2a5..980a7b8d0 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -27,7 +27,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer) import Simplex.Messaging.Server (runSMPServer) import Simplex.Messaging.Server.CLI -import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration) +import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration, defaultProxyClientConcurrency) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange) import Simplex.Messaging.Transport.Client (TransportHost (..)) @@ -156,7 +156,9 @@ smpServerCLI cfgPath logPath = \# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\ \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ \# socks_mode: onion\n\n\ - \[INACTIVE_CLIENTS]\n\ + \# Limit number of threads a client can spawn to process proxy commands in parrallel.\n" + <> ("# client_concurrency: " <> show defaultProxyClientConcurrency <> "\n\n") + <> "[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") @@ -251,7 +253,8 @@ smpServerCLI cfgPath logPath = ownServerDomains = either (const []) textToOwnServers $ lookupValue "PROXY" "own_server_domains" ini, persistErrorInterval = 30 -- seconds }, - allowSMPProxy = True + allowSMPProxy = True, + serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini } textToSocksMode :: Text -> SocksMode textToSocksMode = \case diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index d2e11d29b..99633ac1d 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -119,7 +119,8 @@ cfg = transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedSMPHandshakes}, controlPort = Nothing, smpAgentCfg = defaultSMPClientAgentConfig, - allowSMPProxy = False + allowSMPProxy = False, + serverClientConcurrency = 2 } cfgV7 :: ServerConfig From 2f2a3acdc0840b07bfc3198cf9fc2f398297b016 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 21 May 2024 14:51:11 +0100 Subject: [PATCH 053/125] 5.8.0.3 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 7667d41ef..625c30df4 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.2 +version: 5.8.0.3 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index d8cc1b54d..265df1772 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.2 +version: 5.8.0.3 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From f50589b31a5d25032a0c8ad7e3a25010d4757b3d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 21 May 2024 22:52:22 +0100 Subject: [PATCH 054/125] agent: remove external timeout to resubscribe (#1164) * agent: remove external timeout to resubscribe * liftIO * fix tests --- src/Simplex/Messaging/Agent/Client.hs | 52 ++++++++--------------- src/Simplex/Messaging/Agent/Env/SQLite.hs | 4 -- tests/AgentTests.hs | 28 ++++++------ tests/AgentTests/FunctionalAPITests.hs | 1 + tests/AgentTests/NotificationTests.hs | 17 ++++---- 5 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 2299b2183..30e1f0aa8 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -608,12 +608,11 @@ resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = atomically $ putTMVar (sessionVar v) a runSubWorker = do ri <- asks $ reconnectInterval . config - timeoutCounts <- newTVarIO 0 withRetryInterval ri $ \_ loop -> do pending <- atomically getPending forM_ (L.nonEmpty pending) $ \qs -> do - lift $ waitForUserNetwork c - void . tryAgentError' $ reconnectSMPClient timeoutCounts c tSess qs + liftIO $ waitForUserNetwork c + reconnectSMPClient c tSess qs loop getPending = RQ.getSessQueues tSess $ pendingSubs c cleanup :: SessionVar (Async ()) -> STM () @@ -623,38 +622,23 @@ resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = whenM (isEmptyTMVar $ sessionVar v) retry removeSessVar v tSess smpSubWorkers -reconnectSMPClient :: TVar Int -> AgentClient -> SMPTransportSession -> NonEmpty RcvQueue -> AM () -reconnectSMPClient tc c tSess@(_, srv, _) qs = do - NetworkConfig {tcpTimeout} <- atomically $ getNetworkConfig c - -- this allows 3x of timeout per batch of subscription (90 queues per batch empirically) - let t = (length qs `div` 90 + 1) * tcpTimeout * 3 - ExceptT (sequence <$> (t `timeout` runExceptT resubscribe)) >>= \case - Just _ -> resetTimeouts - -- reset and do not report consecutive timeouts while offline - Nothing -> ifM (atomically $ isNetworkOnline c) notifyTimeout resetTimeouts +reconnectSMPClient :: AgentClient -> SMPTransportSession -> NonEmpty RcvQueue -> AM' () +reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do + cs <- readTVarIO $ RQ.getConnections $ activeSubs c + rs <- subscribeQueues c $ L.toList qs + let (errs, okConns) = partitionEithers $ map (\(RcvQueue {connId}, r) -> bimap (connId,) (const connId) r) rs + conns = filter (`M.notMember` cs) okConns + unless (null conns) $ notifySub "" $ UP srv conns + let (tempErrs, finalErrs) = partition (temporaryAgentError . snd) errs + mapM_ (\(connId, e) -> notifySub connId $ ERR e) finalErrs + forM_ (listToMaybe tempErrs) $ \(connId, e) -> do + when (null okConns && M.null cs && null finalErrs) . liftIO $ + closeClient c smpClients tSess + notifySub connId $ ERR e where - resetTimeouts = atomically $ writeTVar tc 0 - notifyTimeout = do - tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) - maxTC <- asks $ maxSubscriptionTimeouts . config - when (tc' >= maxTC) $ do - let msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess - atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL msg) - resubscribe :: AM () - resubscribe = do - cs <- readTVarIO $ RQ.getConnections $ activeSubs c - rs <- lift . subscribeQueues c $ L.toList qs - let (errs, okConns) = partitionEithers $ map (\(RcvQueue {connId}, r) -> bimap (connId,) (const connId) r) rs - liftIO $ do - let conns = filter (`M.notMember` cs) okConns - unless (null conns) $ notifySub "" $ UP srv conns - let (tempErrs, finalErrs) = partition (temporaryAgentError . snd) errs - liftIO $ mapM_ (\(connId, e) -> notifySub connId $ ERR e) finalErrs - forM_ (listToMaybe tempErrs) $ \(_, err) -> do - when (null okConns && M.null cs && null finalErrs) . liftIO $ - closeClient c smpClients tSess - throwError err - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () + handleNotify :: AM' () -> AM' () + handleNotify = E.handleAny $ notifySub "" . ERR . INTERNAL . show + notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> AM' () notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 9613adf3c..b753a4226 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -102,7 +102,6 @@ data AgentConfig = AgentConfig cleanupInterval :: Int64, cleanupStepInterval :: Int, maxWorkerRestartsPerMin :: Int, - maxSubscriptionTimeouts :: Int, storedMsgDataTTL :: NominalDiffTime, rcvFilesTTL :: NominalDiffTime, sndFilesTTL :: NominalDiffTime, @@ -173,9 +172,6 @@ defaultAgentConfig = cleanupInterval = 30 * 60 * 1000000, -- 30 minutes cleanupStepInterval = 200000, -- 200ms maxWorkerRestartsPerMin = 5, - -- 5 consecutive subscription timeouts will result in alert to the user - -- this is a fallback, as the timeout set to 3x of expected timeout, to avoid potential locking. - maxSubscriptionTimeouts = 5, storedMsgDataTTL = 21 * nominalDay, rcvFilesTTL = 2 * nominalDay, sndFilesTTL = nominalDay, diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 32610b54e..9572aed2f 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -95,23 +95,24 @@ type AEntityTransmission p e = (ACorrId, ConnId, ACommand p e) type AEntityTransmissionOrError p e = (ACorrId, ConnId, Either AgentErrorType (ACommand p e)) tGetAgent :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AEConn) -tGetAgent = tGetAgent' +tGetAgent = tGetAgent' True -tGetAgent' :: forall c e. (Transport c, AEntityI e) => c -> IO (AEntityTransmissionOrError 'Agent e) -tGetAgent' h = do - (corrId, connId, cmdOrErr) <- pGetAgent h +tGetAgent' :: forall c e. (Transport c, AEntityI e) => Bool -> c -> IO (AEntityTransmissionOrError 'Agent e) +tGetAgent' skipErr h = do + (corrId, connId, cmdOrErr) <- pGetAgent skipErr h case cmdOrErr of Right (APC e cmd) -> case testEquality e (sAEntity @e) of Just Refl -> pure (corrId, connId, Right cmd) _ -> error $ "unexpected command " <> show cmd Left err -> pure (corrId, connId, Left err) -pGetAgent :: forall c. Transport c => c -> IO (ATransmissionOrError 'Agent) -pGetAgent h = do +pGetAgent :: forall c. Transport c => Bool -> c -> IO (ATransmissionOrError 'Agent) +pGetAgent skipErr h = do (corrId, connId, cmdOrErr) <- tGet SAgent h case cmdOrErr of - Right (APC _ CONNECT {}) -> pGetAgent h - Right (APC _ DISCONNECT {}) -> pGetAgent h + Right (APC _ CONNECT {}) -> pGetAgent skipErr h + Right (APC _ DISCONNECT {}) -> pGetAgent skipErr h + Right (APC _ (ERR (BROKER _ NETWORK))) | skipErr -> pGetAgent skipErr h cmd -> pure (corrId, connId, cmd) -- | receive message to handle `h` @@ -119,15 +120,18 @@ pGetAgent h = do (<#:) = tGetAgent (<#:?) :: Transport c => c -> IO (ATransmissionOrError 'Agent) -(<#:?) = pGetAgent +(<#:?) = pGetAgent True (<#:.) :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AENone) -(<#:.) = tGetAgent' +(<#:.) = tGetAgent' True -- | send transmission `t` to handle `h` and get response (#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (AEntityTransmissionOrError 'Agent 'AEConn) h #: t = tPutRaw h t >> (<#:) h +(#:!) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (AEntityTransmissionOrError 'Agent 'AEConn) +h #:! t = tPutRaw h t >> tGetAgent' False h + -- | action and expected response -- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r` (#>) :: IO (AEntityTransmissionOrError 'Agent 'AEConn) -> AEntityTransmission 'Agent 'AEConn -> Expectation @@ -426,8 +430,8 @@ testServerConnectionAfterError t _ = do withAgent1 $ \bob -> do withAgent2 $ \alice -> do - bob #: ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT - alice #: ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT + bob #:! ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT + alice #:! ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT withServer $ do alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 7cf1ab00a..bb1d34b10 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -144,6 +144,7 @@ pGet c = do case cmd of CONNECT {} -> pGet c DISCONNECT {} -> pGet c + ERR (BROKER _ NETWORK) -> pGet c _ -> pure t pattern CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> ACommand 'Agent e diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 2c1045791..cf1c4783a 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.NotificationTests where @@ -17,10 +17,6 @@ import AgentTests.FunctionalAPITests createConnection, exchangeGreetingsMsgId, get, - withAgent, - withAgentClients2, - withAgentClientsCfgServers2, - withAgentClients3, joinConnection, makeConnection, nGet, @@ -29,7 +25,11 @@ import AgentTests.FunctionalAPITests sendMessage, switchComplete, testServerMatrix2, + withAgent, + withAgentClients2, + withAgentClients3, withAgentClientsCfg2, + withAgentClientsCfgServers2, (##>), (=##>), pattern CON, @@ -59,8 +59,8 @@ import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO) import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Protocol +import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Types (NtfToken (..)) import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) @@ -151,7 +151,8 @@ testNtfMatrix t runTest = do it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest skip "this case cannot be supported - see RFC" $ - it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest + it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ + runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest -- servers can be migrated in any order it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest @@ -258,7 +259,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do atomically $ readTBQueue apnsQ liftIO $ sendApnsResponse APNSRespOk pure ntfData - -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server + -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server threadDelay 1000000 withAgent 2 agentCfg initAgentServers testDB $ \a' -> -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, From 769e54db76fc33a04085410cdf78ae77ed9717b6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 21 May 2024 22:58:58 +0100 Subject: [PATCH 055/125] 5.7.4.1 --- CHANGELOG.md | 7 +++++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0093ba92..e2a7bb5b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 5.7.4 + +SMP agent: +- remove re-subscription timeouts (as they are tracked per operation, and could cause failed subscriptions). +- reconnect XFTP clients when network settings changes. +- fix lock contention resulting in stuck subscriptions on network change. + # 5.7.3 SMP/NTF protocol: diff --git a/package.yaml b/package.yaml index 7ba64f531..a1f4a225a 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.4.0 +version: 5.7.4.1 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 19c99acc4..9bfa146c1 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.4.0 +version: 5.7.4.1 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 8097df5540f149347676f141b39dc27295530f30 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 22 May 2024 13:25:49 +0100 Subject: [PATCH 056/125] agent: persist server connection error (#1165) * agent: persist server connection * comment, refactor * fix tests, reduce interval for ntf server * cleanup * 0 --- src/Simplex/Messaging/Agent/Client.hs | 49 +++++++++++++------ src/Simplex/Messaging/Agent/Env/SQLite.hs | 2 + src/Simplex/Messaging/Client/Agent.hs | 20 ++++---- .../Messaging/Notifications/Server/Main.hs | 4 +- tests/AgentTests/FunctionalAPITests.hs | 37 +++++--------- tests/AgentTests/NotificationTests.hs | 1 + tests/NtfClient.hs | 6 ++- tests/SMPAgentClient.hs | 1 + tests/SMPClient.hs | 10 ++-- tests/XFTPAgent.hs | 2 +- 10 files changed, 70 insertions(+), 62 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 7d07e7366..a2d7b5f59 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -175,7 +175,7 @@ import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import Data.Text.Encoding -import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime) +import Data.Time (UTCTime, addUTCTime, defaultTimeLocale, formatTime, getCurrentTime) import Data.Time.Clock.System (getSystemTime) import Data.Word (Word16) import qualified Database.SQLite.Simple as SQL @@ -252,7 +252,7 @@ import UnliftIO.Directory (doesFileExist, getTemporaryDirectory, removeFile) import qualified UnliftIO.Exception as E import UnliftIO.STM -type ClientVar msg = SessionVar (Either AgentErrorType (Client msg)) +type ClientVar msg = SessionVar (Either (AgentErrorType, Maybe UTCTime) (Client msg)) type SMPClientVar = ClientVar SMP.BrokerMsg @@ -584,7 +584,7 @@ getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClien getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do unlessM (readTVarIO active) . throwError $ INACTIVE atomically (getSessVar workerSeq tSess smpClients) - >>= either newClient (waitForProtocolClient c tSess) + >>= either newClient (waitForProtocolClient c tSess smpClients) where newClient v = do prs <- atomically TM.empty @@ -613,7 +613,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq (clnt,) <$> newProxiedRelay clnt auth rv waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) waitForProxyClient tSess auth v = do - clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess v + clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess smpClients v sess <- atomically (getSessVar workerSeq destSrv prs) >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) @@ -744,7 +744,7 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, atomically (getSessVar workerSeq tSess ntfClients) >>= either (newProtocolClient c tSess ntfClients connectClient) - (waitForProtocolClient c tSess) + (waitForProtocolClient c tSess ntfClients) where connectClient :: NtfClientVar -> AM NtfClient connectClient v = do @@ -767,7 +767,7 @@ getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId atomically (getSessVar workerSeq tSess xftpClients) >>= either (newProtocolClient c tSess xftpClients connectClient) - (waitForProtocolClient c tSess) + (waitForProtocolClient c tSess xftpClients) where connectClient :: XFTPClientVar -> AM XFTPClient connectClient v = do @@ -784,14 +784,26 @@ getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv -waitForProtocolClient :: ProtocolTypeI (ProtoType msg) => AgentClient -> TransportSession msg -> ClientVar msg -> AM (Client msg) -waitForProtocolClient c (_, srv, _) v = do +waitForProtocolClient :: + (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => + AgentClient -> + TransportSession msg -> + TMap (TransportSession msg) (ClientVar msg) -> + ClientVar msg -> + AM (Client msg) +waitForProtocolClient c tSess@(_, srv, _) clients v = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c client_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) - liftEither $ case client_ of - Just (Right smpClient) -> Right smpClient - Just (Left e) -> Left e - Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT + case client_ of + Just (Right smpClient) -> pure smpClient + Just (Left (e, ts_)) -> case ts_ of + Nothing -> throwE e + Just ts -> + ifM + ((ts <) <$> liftIO getCurrentTime) + (atomically (removeSessVar v tSess clients) >> getProtocolServerClient c tSess) + (throwE e) + Nothing -> throwE $ BROKER (B.unpack $ strEncode srv) TIMEOUT -- clientConnected arg is only passed for SMP server newProtocolClient :: @@ -813,10 +825,15 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = pure client Left e -> do liftIO $ incServerStat c userId srv "CLIENT" $ strEncode e - atomically $ do - removeSessVar v tSess clients - putTMVar (sessionVar v) (Left e) - throwError e -- signal error to caller + ei <- asks $ persistErrorInterval . config + if ei == 0 + then atomically $ do + removeSessVar v tSess clients + putTMVar (sessionVar v) (Left (e, Nothing)) + else do + ts <- addUTCTime ei <$> liftIO getCurrentTime + atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) + throwE e -- signal error to caller hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> Client msg -> ACommand 'Agent 'AENone hostEvent event = hostEvent' event . protocolClient diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index b753a4226..ccefe9c22 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -98,6 +98,7 @@ data AgentConfig = AgentConfig connDeleteDeliveryTimeout :: NominalDiffTime, helloTimeout :: NominalDiffTime, quotaExceededTimeout :: NominalDiffTime, + persistErrorInterval :: NominalDiffTime, initialCleanupDelay :: Int64, cleanupInterval :: Int64, cleanupStepInterval :: Int, @@ -168,6 +169,7 @@ defaultAgentConfig = connDeleteDeliveryTimeout = 2 * nominalDay, helloTimeout = 2 * nominalDay, quotaExceededTimeout = 7 * nominalDay, + persistErrorInterval = 10, -- seconds initialCleanupDelay = 30 * 1000000, -- 30 seconds cleanupInterval = 30 * 60 * 1000000, -- 30 minutes cleanupStepInterval = 200000, -- 200ms diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index a7732b4d4..f27de3667 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -89,7 +89,7 @@ defaultSMPClientAgentConfig = increaseAfter = 10 * second, maxInterval = 10 * second }, - persistErrorInterval = 0, + persistErrorInterval = 30, -- seconds msgQSize = 256, agentQSize = 256, agentSubsBatchSize = 900, @@ -183,12 +183,13 @@ getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worke smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) case smpClient_ of Just (Right smpClient) -> pure smpClient - Just (Left (e, Nothing)) -> throwE e - Just (Left (e, Just ts)) -> - ifM - ((ts <) <$> liftIO getCurrentTime) - (atomically (removeSessVar v srv smpClients) >> getSMPServerClient'' ca srv) - (throwE e) + Just (Left (e, ts_)) -> case ts_ of + Nothing -> throwE e + Just ts -> + ifM + ((ts <) <$> liftIO getCurrentTime) + (atomically (removeSessVar v srv smpClients) >> getSMPServerClient'' ca srv) + (throwE e) Nothing -> throwE PCEResponseTimeout newSMPClient :: SMPClientVar -> IO (Either SMPClientError (OwnServer, SMPClient)) @@ -204,12 +205,13 @@ getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worke notify ca $ CAConnected srv pure $ Right c Left e -> do - if persistErrorInterval agentCfg == 0 || e == PCENetworkError || e == PCEResponseTimeout + let ei = persistErrorInterval agentCfg + if ei == 0 then atomically $ do putTMVar (sessionVar v) (Left (e, Nothing)) removeSessVar v srv smpClients else do - ts <- addUTCTime (persistErrorInterval agentCfg) <$> liftIO getCurrentTime + ts <- addUTCTime ei <$> liftIO getCurrentTime atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) reconnectClient ca srv pure $ Left e diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index 0efb7d599..b28dd6111 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -13,7 +13,7 @@ import Data.Maybe (fromMaybe) import qualified Data.Text as T import Network.Socket (HostName) import Options.Applicative -import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Server (runNtfServer) import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration) @@ -113,7 +113,7 @@ ntfServerCLI cfgPath logPath = clientQSize = 64, subQSize = 512, pushQSize = 1048, - smpAgentCfg = defaultSMPClientAgentConfig, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 15}, apnsConfig = defaultAPNSPushClientConfig, subsBatchSize = 900, inactiveClientExpiration = diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index ef283a7d9..2ece3e7b9 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -236,10 +236,10 @@ runRight action = getInAnyOrder :: HasCallStack => AgentClient -> [ATransmission 'Agent -> Bool] -> Expectation getInAnyOrder c ts = withFrozenCallStack $ inAnyOrder (pGet c) ts -inAnyOrder :: (Show a, MonadIO m, HasCallStack) => m a -> [a -> Bool] -> m () +inAnyOrder :: (Show a, MonadUnliftIO m, HasCallStack) => m a -> [a -> Bool] -> m () inAnyOrder _ [] = pure () inAnyOrder g rs = withFrozenCallStack $ do - r <- g + r <- 5000000 `timeout` g >>= maybe (error "inAnyOrder timeout") pure let rest = filter (not . expected r) rs if length rest < length rs then inAnyOrder g rest @@ -1264,15 +1264,10 @@ testRatchetSyncServerOffline t = withAgentClients2 $ \alice bob -> do liftIO $ ratchetSyncState `shouldBe` RSStarted withSmpServerStoreMsgLogOn t testPort $ \_ -> do + concurrently_ + (getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP]) + (getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP]) runRight_ $ do - liftIO . getInAnyOrder alice $ - [ ratchetSyncP' bobId RSAgreed, - serverUpP - ] - liftIO . getInAnyOrder bob2 $ - [ ratchetSyncP' aliceId RSAgreed, - serverUpP - ] get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 @@ -1326,15 +1321,10 @@ testRatchetSyncSuspendForeground t = do foregroundAgent bob2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do + concurrently_ + (getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP]) + (getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP]) runRight_ $ do - liftIO . getInAnyOrder alice $ - [ ratchetSyncP' bobId RSAgreed, - serverUpP - ] - liftIO . getInAnyOrder bob2 $ - [ ratchetSyncP' aliceId RSAgreed, - serverUpP - ] get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 @@ -1359,15 +1349,10 @@ testRatchetSyncSimultaneous t = do liftIO $ aRSS `shouldBe` RSStarted withSmpServerStoreMsgLogOn t testPort $ \_ -> do + concurrently_ + (getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP]) + (getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP]) runRight_ $ do - liftIO . getInAnyOrder alice $ - [ ratchetSyncP' bobId RSAgreed, - serverUpP - ] - liftIO . getInAnyOrder bob2 $ - [ ratchetSyncP' aliceId RSAgreed, - serverUpP - ] get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 04b7fc69c..348694f7e 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -335,6 +335,7 @@ testNtfTokenChangeServers t APNSMockServer {apnsQ} = getTestNtfTokenPort a >>= \port2 -> liftIO $ port2 `shouldBe` ntfTestPort2 -- but the token got updated killThread ntf withNtfServerOn t ntfTestPort2 $ runRight_ $ do + liftIO $ threadDelay 1000000 -- for notification server to reconnect tkn <- registerTestToken a "qwer" NMInstant apnsQ checkNtfToken a tkn >>= \r -> liftIO $ r `shouldBe` NTActive diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index 564523e0b..f5b8a4b2e 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -89,7 +89,7 @@ ntfServerCfg = clientQSize = 1, subQSize = 1, pushQSize = 1, - smpAgentCfg = defaultSMPClientAgentConfig, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 1}, apnsConfig = defaultAPNSPushClientConfig { apnsPort = apnsTestPort, @@ -115,9 +115,11 @@ ntfServerCfgV2 :: NtfServerConfig ntfServerCfgV2 = ntfServerCfg { ntfServerVRange = mkVersionRange initialNTFVersion authBatchCmdsNTFVersion, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}}, + smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}}, Env.transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedNTFHandshakes} } + where + smpAgentCfg' = smpAgentCfg ntfServerCfg withNtfServerStoreLog :: ATransport -> (ThreadId -> IO a) -> IO a withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t)]} diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 3cf09e5db..aee3c8cb0 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -212,6 +212,7 @@ agentCfg = smpCfg = defaultSMPClientConfig {qSize = 1, defaultTransport = (testPort, transport @TLS), networkConfig}, ntfCfg = defaultNTFClientConfig {qSize = 1, defaultTransport = (ntfTestPort, transport @TLS), networkConfig}, reconnectInterval = fastRetryInterval, + persistErrorInterval = 1, xftpNotifyErrsOnRetry = False, ntfWorkerDelay = 100, ntfSMPWorkerDelay = 100, diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 99633ac1d..1828ffae6 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -118,7 +118,7 @@ cfg = smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedSMPHandshakes}, controlPort = Nothing, - smpAgentCfg = defaultSMPClientAgentConfig, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 1}, -- seconds allowSMPProxy = False, serverClientConcurrency = 2 } @@ -134,12 +134,10 @@ proxyCfg = cfgV7 { allowSMPProxy = True, smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, - smpAgentCfg = - defaultSMPClientAgentConfig - { smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True}, - persistErrorInterval = 3 -- seconds - } + smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {serverVRange = proxyVRange, agreeSecret = True}} } + where + smpAgentCfg' = smpAgentCfg cfgV7 proxyVRange :: VersionRangeSMP proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 8f42ec16a..4685d4815 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -54,7 +54,7 @@ xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do it "should resume receiving file after restart" testXFTPAgentReceiveRestore it "should cleanup rcv tmp path after permanent error" testXFTPAgentReceiveCleanup it "should resume sending file after restart" testXFTPAgentSendRestore - it "should cleanup snd prefix path after permanent error" testXFTPAgentSendCleanup + xit "should cleanup snd prefix path after permanent error" testXFTPAgentSendCleanup it "should delete sent file on server" testXFTPAgentDelete it "should resume deleting file after restart" testXFTPAgentDeleteRestore -- TODO when server is fixed to correctly send AUTH error, this test has to be modified to expect AUTH error From 2b09ada39237baaa024447ab8d7bdccef53a0687 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 22 May 2024 14:36:54 +0100 Subject: [PATCH 057/125] agent: reduce interval for storing server connection errors to 3 seconds --- src/Simplex/Messaging/Agent/Env/SQLite.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index ccefe9c22..30b2b566a 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -169,7 +169,7 @@ defaultAgentConfig = connDeleteDeliveryTimeout = 2 * nominalDay, helloTimeout = 2 * nominalDay, quotaExceededTimeout = 7 * nominalDay, - persistErrorInterval = 10, -- seconds + persistErrorInterval = 3, -- seconds initialCleanupDelay = 30 * 1000000, -- 30 seconds cleanupInterval = 30 * 60 * 1000000, -- 30 minutes cleanupStepInterval = 200000, -- 200ms From 6c86aa302f5efc037cbc9377ae85696180cb08ab Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 22 May 2024 16:13:26 +0100 Subject: [PATCH 058/125] 5.8.0.4 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 625c30df4..be2286377 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.3 +version: 5.8.0.4 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 265df1772..e0fb278a1 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.3 +version: 5.8.0.4 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From f6bb105536103d10a4d1e8ec8ed043a1b9e1345c Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Thu, 23 May 2024 14:47:30 +0300 Subject: [PATCH 059/125] utils: remove MonadError usage (#1168) --- src/Simplex/Messaging/Util.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Simplex/Messaging/Util.hs b/src/Simplex/Messaging/Util.hs index b42d5b378..ef2cc6933 100644 --- a/src/Simplex/Messaging/Util.hs +++ b/src/Simplex/Messaging/Util.hs @@ -97,15 +97,15 @@ catchAll_ :: IO a -> IO a -> IO a catchAll_ a = catchAll a . const {-# INLINE catchAll_ #-} -tryAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> m (Either e a) -tryAllErrors err action = tryError action `UE.catch` (pure . Left . err) +tryAllErrors :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> ExceptT e m (Either e a) +tryAllErrors err action = ExceptT $ Right <$> runExceptT action `UE.catch` (pure . Left . err) {-# INLINE tryAllErrors #-} tryAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> m (Either e a) tryAllErrors' err action = runExceptT action `UE.catch` (pure . Left . err) {-# INLINE tryAllErrors' #-} -catchAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> (e -> m a) -> m a +catchAllErrors :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> (e -> ExceptT e m a) -> ExceptT e m a catchAllErrors err action handler = tryAllErrors err action >>= either handler pure {-# INLINE catchAllErrors #-} @@ -113,11 +113,11 @@ catchAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> catchAllErrors' err action handler = tryAllErrors' err action >>= either handler pure {-# INLINE catchAllErrors' #-} -catchThrow :: (MonadUnliftIO m, MonadError e m) => m a -> (E.SomeException -> e) -> m a +catchThrow :: MonadUnliftIO m => ExceptT e m a -> (E.SomeException -> e) -> ExceptT e m a catchThrow action err = catchAllErrors err action throwError {-# INLINE catchThrow #-} -allFinally :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> m b -> m a +allFinally :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> ExceptT e m b -> ExceptT e m a allFinally err action final = tryAllErrors err action >>= \r -> final >> either throwError pure r {-# INLINE allFinally #-} From 5d38ad03af14348febf48a4771d2c79f807fbc0f Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Thu, 23 May 2024 17:34:25 +0300 Subject: [PATCH 060/125] tests: add proxy stress tests (#1163) * tests: add proxy stress tests * organize benches * add agent tests * move prints to logNote * fix stuck agent tests --- tests/SMPProxyTests.hs | 172 ++++++++++++++++++++++++++++++++++------- tests/Util.hs | 22 ++++++ 2 files changed, 165 insertions(+), 29 deletions(-) diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 1c458a062..748eb34e7 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -8,13 +8,14 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} module SMPProxyTests where import AgentTests.FunctionalAPITests -import Control.Monad.Trans.Except (runExceptT) +import Control.Logger.Simple +import Control.Monad (forM, forM_, forever) +import Control.Monad.Trans.Except (ExceptT, runExceptT) import Data.ByteString.Char8 (ByteString) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L @@ -33,9 +34,12 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport +import Simplex.Messaging.Util (bshow, tshow) import Simplex.Messaging.Version (mkVersionRange) +import System.FilePath (splitExtensions) import Test.Hspec import UnliftIO +import Util smpProxyTests :: Spec smpProxyTests = do @@ -71,6 +75,22 @@ smpProxyTests = do deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg1 msg2 it "max message size, X25519 keys" . twoServersFirstProxy $ deliverMessageViaProxy proxyServ relayServ C.SX25519 msg1 msg2 + describe "stress test 1k" $ do + let deliver n = deliverMessagesViaProxy srv1 srv2 C.SEd448 [] (map bshow [1 :: Int .. n]) + it "1x1000" . twoServersFirstProxy $ deliver 1000 + it "5x200" . twoServersFirstProxy $ 5 `inParrallel` deliver 200 + it "10x100" . twoServersFirstProxy $ 10 `inParrallel` deliver 100 + xdescribe "stress test 10k" $ do + let deliver n = deliverMessagesViaProxy srv1 srv2 C.SEd448 [] (map bshow [1 :: Int .. n]) + it "1x10000" . twoServersFirstProxy $ deliver 10000 + it "5x2000" . twoServersFirstProxy $ 5 `inParrallel` deliver 2000 + it "10x1000" . twoServersFirstProxy $ 10 `inParrallel` deliver 1000 + it "100x100 N1" . twoServersFirstProxy $ withNumCapabilities 1 $ 100 `inParrallel` deliver 100 + it "100x100 N4 C1" . twoServersNoConc $ withNumCapabilities 4 $ 100 `inParrallel` deliver 100 + it "100x100 N4 C2" . twoServersFirstProxy $ withNumCapabilities 4 $ 100 `inParrallel` deliver 100 + it "100x100 N4 C16" . twoServersMoreConc $ withNumCapabilities 4 $ 100 `inParrallel` deliver 100 + it "100x100 N" . twoServersFirstProxy $ withNCPUCapabilities $ 100 `inParrallel` deliver 100 + it "500x20" . twoServersFirstProxy $ 500 `inParrallel` deliver 20 describe "agent API" $ do describe "one server" $ do it "always via proxy" . oneServer $ @@ -92,46 +112,65 @@ smpProxyTests = do agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ agentViaProxyVersionError + describe "stress test 1k" $ do + let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) + it "2 agents, 250 messages" . oneServer $ deliver 2 250 + it "5 agents, 10 pairs, 50 messages, N1" . oneServer . withNumCapabilities 1 $ deliver 5 50 + it "5 agents, 10 pairs, 50 messages. N4" . oneServer . withNumCapabilities 4 $ deliver 5 50 + xdescribe "stress test 10k" $ do + let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) + it "25 agents, 300 pairs, 17 messages" . oneServer . withNumCapabilities 4 $ deliver 25 17 where - oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg testPort . const + oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg {msgQueueQuota = 128} testPort . const twoServers = twoServers_ proxyCfg proxyCfg - twoServersFirstProxy = twoServers_ proxyCfg cfgV8 + twoServersFirstProxy = twoServers_ proxyCfg cfgV8 {msgQueueQuota = 128} + twoServersMoreConc = twoServers_ proxyCfg {serverClientConcurrency = 128} cfgV8 {msgQueueQuota = 128} + twoServersNoConc = twoServers_ proxyCfg {serverClientConcurrency = 1} cfgV8 {msgQueueQuota = 128} twoServers_ cfg1 cfg2 runTest = withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO () -deliverMessageViaProxy proxyServ relayServ alg msg msg' = do +deliverMessageViaProxy proxyServ relayServ alg msg msg' = deliverMessagesViaProxy proxyServ relayServ alg [msg] [msg'] + +deliverMessagesViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> [ByteString] -> [ByteString] -> IO () +deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do g <- C.newRandom -- set up proxy - Right pc <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ()) + pc' <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ()) + pc <- either (fail . show) pure pc' THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc -- set up relay - msgQ <- newTBQueueIO 4 - Right rc <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ()) - runRight_ $ do - -- prepare receiving queue - (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g - (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe - let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv - -- get proxy session - sess <- connectSMPProxiedRelay pc relayServ (Just "correct") - -- send via proxy to unsecured queue - Right () <- proxySMPMessage pc sess Nothing sndId noMsgFlags msg + msgQ <- newTBQueueIO 1024 + rc' <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ()) + rc <- either (fail . show) pure rc' + -- prepare receiving queue + (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g + (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g + QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe + let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv + -- get proxy session + sess <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") + -- send via proxy to unsecured queue + forM_ unsecuredMsgs $ \msg -> do + runExceptT' (proxySMPMessage pc sess Nothing sndId noMsgFlags msg) `shouldReturn` Right () -- receive 1 (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody})))]) <- atomically $ readTBQueue msgQ - liftIO $ dec msgId encBody `shouldBe` Right msg - ackSMPMessage rc rPriv rcvId msgId - -- secure queue - (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g - secureSMPQueue rc rPriv rcvId sPub - -- send via proxy to secured queue - Right () <- proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg' - -- receive 2 - (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'})))]) <- atomically $ readTBQueue msgQ - liftIO $ dec msgId' encBody' `shouldBe` Right msg' - ackSMPMessage rc rPriv rcvId msgId' + dec msgId encBody `shouldBe` Right msg + runExceptT' $ ackSMPMessage rc rPriv rcvId msgId + -- secure queue + (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g + runExceptT' $ secureSMPQueue rc rPriv rcvId sPub + -- send via proxy to secured queue + waitSendRecv + ( forM_ securedMsgs $ \msg' -> + runExceptT' (proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg') `shouldReturn` Right () + ) + ( forM_ securedMsgs $ \msg' -> do + (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'})))]) <- atomically $ readTBQueue msgQ + dec msgId' encBody' `shouldBe` Right msg' + runExceptT' $ ackSMPMessage rc rPriv rcvId msgId' + ) agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> IO () agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 = @@ -171,6 +210,71 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} +agentDeliverMessagesViaProxyConc :: [NonEmpty SMPServer] -> [MsgBody] -> IO () +agentDeliverMessagesViaProxyConc agentServers msgs = + withAgents $ \agents -> do + let pairs = combinations 2 agents + logNote $ "Pairing " <> tshow (length agents) <> " agents into " <> tshow (length pairs) <> " connections" + connections <- forM pairs $ \case + [a, b] -> prePair a b + _ -> error "agents must be paired" + logNote "Running..." + mapConcurrently_ run connections + where + withAgents :: ([AgentClient] -> IO ()) -> IO () + withAgents action = go [] (zip [1 :: Int ..] agentServers) + where + go agents = \case + [] -> action agents + (aId, aSrvs) : next -> withAgent aId aCfg (servers aSrvs) (dbPrefix <> show aId <> dbSuffix) $ \a -> (a : agents) `go` next + (dbPrefix, dbSuffix) = splitExtensions testDB + -- agent connections have to be set up in advance + -- otherwise the CONF messages would get mixed with MSG + prePair alice bob = do + (bobId, qInfo) <- runExceptT' $ A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- runExceptT' $ A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + confId <- + get alice >>= \case + ("", _, A.CONF confId pqSup' _ "bob's connInfo") -> do + pqSup' `shouldBe` PQSupportOn + pure confId + huh -> fail $ show huh + runExceptT' $ allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + pure (alice, bobId, bob, aliceId) + -- stream messages in opposite directions, while getting deliveries and sending ACKs + run (alice, bobId, bob, aliceId) = do + aSender <- async $ forM_ msgs $ runExceptT' . A.sendMessage alice bobId pqEnc noMsgFlags + bRecipient <- + async $ + forever $ + get bob >>= \case + ("", _, A.SENT _ _) -> pure () + ("", _, Msg' mId' _ _) -> runExceptT' $ ackMessage alice bobId mId' Nothing + huh -> fail (show huh) + bSender <- async $ forM_ msgs $ runExceptT' . A.sendMessage bob aliceId pqEnc noMsgFlags + aRecipient <- + async $ + forever $ + get alice >>= \case + ("", _, A.SENT _ _) -> pure () + ("", _, Msg' mId' _ _) -> runExceptT' $ ackMessage alice bobId mId' Nothing + huh -> fail (show huh) + logDebug "run waiting..." + a2b <- async $ (waitCatch aSender >>= either throwIO pure) `finally` cancel bRecipient -- stopped sender cancels paired recipient loop + b2a <- async $ (waitCatch bSender >>= either throwIO pure) `finally` cancel aRecipient + waitEitherCatch a2b b2a >>= \case + Right (Right ()) -> wait b2a + Right (Left e) -> cancel bSender >> throwIO e + Left (Right ()) -> wait a2b + Left (Left e) -> cancel aSender >> throwIO e + logDebug "run finished" + pqEnc = CR.PQEncOn + aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg C.SEd448, rcvAuthAlg = C.AuthAlg C.SEd448} + servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + agentViaProxyVersionError :: IO () agentViaProxyVersionError = withAgent 1 agentProxyCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do @@ -201,3 +305,13 @@ testProxyAuth = do todo :: IO () todo = do fail "TODO" + +runExceptT' :: Exception e => ExceptT e IO a -> IO a +runExceptT' a = runExceptT a >>= either throwIO pure + +waitSendRecv :: IO () -> IO () -> IO () +waitSendRecv s r = do + s' <- async s + r' <- async r + waitCatch s' >>= either (\e -> cancel r' >> fail (show e)) pure + waitCatch r' >>= either (\e -> cancel s' >> fail (show e)) pure diff --git a/tests/Util.hs b/tests/Util.hs index a52fee32c..6ad6d054f 100644 --- a/tests/Util.hs +++ b/tests/Util.hs @@ -1,6 +1,28 @@ module Util where +import Control.Monad (replicateM) +import Data.Either (partitionEithers) +import Data.List (tails) +import GHC.Conc (getNumCapabilities, getNumProcessors, setNumCapabilities) import Test.Hspec +import UnliftIO skip :: String -> SpecWith a -> SpecWith a skip = before_ . pendingWith + +withNumCapabilities :: Int -> IO a -> IO a +withNumCapabilities new a = getNumCapabilities >>= \old -> bracket_ (setNumCapabilities new) (setNumCapabilities old) a + +withNCPUCapabilities :: IO a -> IO a +withNCPUCapabilities a = getNumProcessors >>= \p -> withNumCapabilities p a + +inParrallel :: Int -> IO () -> IO () +inParrallel n action = do + streams <- replicateM n $ async action + (es, rs) <- partitionEithers <$> mapM waitCatch streams + map show es `shouldBe` [] + length rs `shouldBe` n + +combinations :: Int -> [a] -> [[a]] +combinations 0 _ = [[]] +combinations k xs = [y : ys | y : xs' <- tails xs, ys <- combinations (k - 1) xs'] From 984394d90630731dcbc26ba0ffa13bf9fa98a251 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Thu, 23 May 2024 18:44:00 +0300 Subject: [PATCH 061/125] core: remove MonadUnliftIO ExceptT orphans (#1169) --- src/Simplex/FileTransfer/Client.hs | 1 - src/Simplex/FileTransfer/Client/Agent.hs | 1 - src/Simplex/Messaging/Agent/Client.hs | 1 - src/Simplex/Messaging/Agent/Env/SQLite.hs | 1 - .../Messaging/Agent/NtfSubSupervisor.hs | 1 - src/Simplex/Messaging/Client/Agent.hs | 29 ------------------- src/Simplex/Messaging/Crypto/File.hs | 1 - tests/AgentTests/FunctionalAPITests.hs | 18 +++++++++++- tests/CoreTests/UtilTests.hs | 2 +- 9 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 468df6157..e407279ce 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -39,7 +39,6 @@ import Simplex.Messaging.Client proxyUsername, transportClientConfig, ) -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding (smpDecode, smpEncode) diff --git a/src/Simplex/FileTransfer/Client/Agent.hs b/src/Simplex/FileTransfer/Client/Agent.hs index 1dafc8108..c17790c2d 100644 --- a/src/Simplex/FileTransfer/Client/Agent.hs +++ b/src/Simplex/FileTransfer/Client/Agent.hs @@ -18,7 +18,6 @@ import Data.Text.Encoding (decodeUtf8) import Simplex.FileTransfer.Client import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError) -import Simplex.Messaging.Client.Agent () import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtocolServer (..), XFTPServer) import Simplex.Messaging.TMap (TMap) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 30e1f0aa8..e59dee7a1 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -185,7 +185,6 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Agent.TRcvQueues (TRcvQueues (getRcvQueues)) import qualified Simplex.Messaging.Agent.TRcvQueues as RQ import Simplex.Messaging.Client -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index b753a4226..cd32d4e8a 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -54,7 +54,6 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (VersionRangeE2E, supportedE2EEncryptVRange) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) diff --git a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs index 4aaa5f278..1e2c7cb00 100644 --- a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs +++ b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs @@ -33,7 +33,6 @@ import Simplex.Messaging.Agent.Protocol (ACommand (..), APartyCmd (..), AgentErr import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol (NtfSubStatus (..), NtfTknStatus (..), SMPQueueNtf (..)) import Simplex.Messaging.Notifications.Types diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 4b925c6f6..2ade2d24f 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -8,7 +8,6 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} module Simplex.Messaging.Client.Agent where @@ -19,7 +18,6 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Trans.Except -import Control.Monad.Trans.Reader import Crypto.Random (ChaChaDRG) import Data.Bifunctor (bimap, first) import Data.ByteString.Char8 (ByteString) @@ -47,8 +45,6 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Util (catchAll_, toChunks, ($>>=)) import System.Timeout (timeout) import UnliftIO (async) -import UnliftIO.Exception (Exception) -import qualified UnliftIO.Exception as E import UnliftIO.STM type SMPClientVar = SessionVar (Either SMPClientError SMPClient) @@ -105,31 +101,6 @@ data SMPClientAgent = SMPClientAgent workerSeq :: TVar Int } -newtype InternalException e = InternalException {unInternalException :: e} - deriving (Eq, Show) - -instance Exception e => Exception (InternalException e) - -instance Exception e => MonadUnliftIO (ExceptT e IO) where - {-# INLINE withRunInIO #-} - withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b - withRunInIO inner = - ExceptT . fmap (first unInternalException) . E.try $ - withRunInIO $ \run -> - inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT) - --- as MonadUnliftIO instance for IO is `withRunInIO inner = inner id`, --- the last two lines could be replaced with: --- inner $ either (E.throwIO . InternalException) pure <=< runExceptT - -instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where - {-# INLINE withRunInIO #-} - withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b - withRunInIO inner = - withExceptT unInternalException . ExceptT . E.try $ - withRunInIO $ \run -> - inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT) - newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do msgQ <- newTBQueue msgQSize diff --git a/src/Simplex/Messaging/Crypto/File.hs b/src/Simplex/Messaging/Crypto/File.hs index 2787df58e..9608d21b7 100644 --- a/src/Simplex/Messaging/Crypto/File.hs +++ b/src/Simplex/Messaging/Crypto/File.hs @@ -31,7 +31,6 @@ import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (isJust) -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Lazy (LazyByteString) import qualified Simplex.Messaging.Crypto.Lazy as LC diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index bb1d34b10..72a62cbbf 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -1,7 +1,9 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} @@ -54,6 +56,7 @@ import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (isRight) @@ -65,7 +68,7 @@ import Data.Maybe (isJust, isNothing) import qualified Data.Set as S import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.Clock.System (SystemTime (..), getSystemTime) -import Data.Type.Equality +import Data.Type.Equality (testEquality, (:~:) (Refl)) import Data.Word (Word16) import qualified Database.SQLite.Simple as SQL import GHC.Stack (withFrozenCallStack) @@ -2809,3 +2812,16 @@ exchangeGreetingsMsgIds alice bobId aliceMsgId bob aliceId bobMsgId = do get bob ##> ("", aliceId, SENT bobMsgId') get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False ackMessage alice bobId aliceMsgId' Nothing + +newtype InternalException e = InternalException {unInternalException :: e} + deriving (Eq, Show) + +instance Exception e => Exception (InternalException e) + +instance Exception e => MonadUnliftIO (ExceptT e IO) where + {-# INLINE withRunInIO #-} + withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b + withRunInIO inner = + ExceptT . fmap (first unInternalException) . try $ + withRunInIO $ \run -> + inner $ run . (either (throwIO . InternalException) pure <=< runExceptT) diff --git a/tests/CoreTests/UtilTests.hs b/tests/CoreTests/UtilTests.hs index 9e413e838..2254ecafd 100644 --- a/tests/CoreTests/UtilTests.hs +++ b/tests/CoreTests/UtilTests.hs @@ -2,11 +2,11 @@ module CoreTests.UtilTests where +import AgentTests.FunctionalAPITests () import Control.Exception (Exception, SomeException, throwIO) import Control.Monad.Except import Control.Monad.IO.Class import Data.IORef -import Simplex.Messaging.Client.Agent () import Simplex.Messaging.Util import Test.Hspec import qualified UnliftIO.Exception as UE From 6309f92c6860fce39d6675eb92fc460bdc3db01d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 23 May 2024 22:01:57 +0100 Subject: [PATCH 062/125] agent: fail if non-unique connection IDs are passed to sendMessages (#1170) --- src/Simplex/Messaging/Agent.hs | 30 +++++++++++++++++++-------- src/Simplex/Messaging/Agent/Client.hs | 6 +++--- src/Simplex/Messaging/Agent/Lock.hs | 10 ++++----- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 652e2d58d..dddaa7231 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -138,6 +138,8 @@ import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) +import Data.Set (Set) +import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock @@ -354,12 +356,12 @@ sendMessage c = withAgentEnv c .:: sendMessage' c type MsgReq = (ConnId, PQEncryption, MsgFlags, MsgBody) -- | Send multiple messages to different connections (SEND command) -sendMessages :: AgentClient -> [MsgReq] -> IO [Either AgentErrorType (AgentMsgId, PQEncryption)] -sendMessages c = withAgentEnv' c . sendMessages' c +sendMessages :: AgentClient -> [MsgReq] -> AE [Either AgentErrorType (AgentMsgId, PQEncryption)] +sendMessages c = withAgentEnv c . sendMessages' c {-# INLINE sendMessages #-} -sendMessagesB :: Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> IO (t (Either AgentErrorType (AgentMsgId, PQEncryption))) -sendMessagesB c = withAgentEnv' c . sendMessagesB' c +sendMessagesB :: Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> AE (t (Either AgentErrorType (AgentMsgId, PQEncryption))) +sendMessagesB c = withAgentEnv c . sendMessagesB' c {-# INLINE sendMessagesB #-} ackMessage :: AgentClient -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AE () @@ -1033,16 +1035,27 @@ getNotificationMessage' c nonce encNtfInfo = do -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: AgentClient -> ConnId -> PQEncryption -> MsgFlags -> MsgBody -> AM (AgentMsgId, PQEncryption) -sendMessage' c connId pqEnc msgFlags msg = ExceptT $ runIdentity <$> sendMessagesB' c (Identity (Right (connId, pqEnc, msgFlags, msg))) +sendMessage' c connId pqEnc msgFlags msg = ExceptT $ runIdentity <$> sendMessagesB_ c (Identity (Right (connId, pqEnc, msgFlags, msg))) (S.singleton connId) {-# INLINE sendMessage' #-} -- | Send multiple messages to different connections (SEND command) in Reader monad -sendMessages' :: AgentClient -> [MsgReq] -> AM' [Either AgentErrorType (AgentMsgId, PQEncryption)] +sendMessages' :: AgentClient -> [MsgReq] -> AM [Either AgentErrorType (AgentMsgId, PQEncryption)] sendMessages' c = sendMessagesB' c . map Right {-# INLINE sendMessages' #-} -sendMessagesB' :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> AM' (t (Either AgentErrorType (AgentMsgId, PQEncryption))) -sendMessagesB' c reqs = withConnLocks c connIds "sendMessages" $ do +sendMessagesB' :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> AM (t (Either AgentErrorType (AgentMsgId, PQEncryption))) +sendMessagesB' c reqs = do + connIds <- liftEither $ foldl' addConnId (Right S.empty) reqs + lift $ sendMessagesB_ c reqs connIds + where + addConnId s@(Right s') (Right (connId, _, _, _)) + | B.null connId = s + | connId `S.notMember` s' = Right $ S.insert connId s' + | otherwise = Left $ INTERNAL "sendMessages: duplicate connection ID" + addConnId s _ = s + +sendMessagesB_ :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> Set ConnId -> AM' (t (Either AgentErrorType (AgentMsgId, PQEncryption))) +sendMessagesB_ c reqs connIds = withConnLocks c connIds "sendMessages" $ do reqs' <- withStoreBatch c (\db -> fmap (bindRight $ \req@(connId, _, _, _) -> bimap storeError (req,) <$> getConn db connId) reqs) let (toEnable, reqs'') = mapAccumL prepareConn [] reqs' void $ withStoreBatch' c $ \db -> map (\connId -> setConnPQSupport db connId PQSupportOn) toEnable @@ -1064,7 +1077,6 @@ sendMessagesB' c reqs = withConnLocks c connIds "sendMessages" $ do let cData' = cData {pqSupport = PQSupportOn} :: ConnData in (connId : acc, Right (cData', sqs, Just pqEnc, msgFlags, A_MSG msg)) | otherwise = (acc, Right (cData, sqs, Just pqEnc, msgFlags, A_MSG msg)) - connIds = map (\(connId, _, _, _) -> connId) $ rights $ toList reqs -- / async command processing v v v diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index e59dee7a1..2dd4ec3c9 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -826,15 +826,15 @@ withInvLock' :: AgentClient -> ByteString -> String -> AM' a -> AM' a withInvLock' AgentClient {invLocks} = withLockMap invLocks {-# INLINE withInvLock' #-} -withConnLocks :: AgentClient -> [ConnId] -> String -> AM' a -> AM' a -withConnLocks AgentClient {connLocks} = withLocksMap_ connLocks . filter (not . B.null) +withConnLocks :: AgentClient -> Set ConnId -> String -> AM' a -> AM' a +withConnLocks AgentClient {connLocks} = withLocksMap_ connLocks {-# INLINE withConnLocks #-} withLockMap :: (Ord k, MonadUnliftIO m) => TMap k Lock -> k -> String -> m a -> m a withLockMap = withGetLock . getMapLock {-# INLINE withLockMap #-} -withLocksMap_ :: (Ord k, MonadUnliftIO m) => TMap k Lock -> [k] -> String -> m a -> m a +withLocksMap_ :: (Ord k, MonadUnliftIO m) => TMap k Lock -> Set k -> String -> m a -> m a withLocksMap_ = withGetLocks . getMapLock {-# INLINE withLocksMap_ #-} diff --git a/src/Simplex/Messaging/Agent/Lock.hs b/src/Simplex/Messaging/Agent/Lock.hs index c0647b844..69b8169e2 100644 --- a/src/Simplex/Messaging/Agent/Lock.hs +++ b/src/Simplex/Messaging/Agent/Lock.hs @@ -12,6 +12,8 @@ import Control.Monad (void) import Control.Monad.Except (ExceptT (..), runExceptT) import Control.Monad.IO.Unlift import Data.Functor (($>)) +import Data.Set (Set) +import qualified Data.Set as S import UnliftIO.Async (forConcurrently) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -39,13 +41,11 @@ withGetLock getLock key name a = (atomically . takeTMVar) (const a) -withGetLocks :: MonadUnliftIO m => (k -> STM Lock) -> [k] -> String -> m a -> m a +withGetLocks :: MonadUnliftIO m => (k -> STM Lock) -> Set k -> String -> m a -> m a withGetLocks getLock keys name = E.bracket holdLocks releaseLocks . const where - holdLocks = forConcurrently keys $ \key -> atomically $ getPutLock getLock key name - -- only this withGetLocks would be holding the locks, - -- so it's safe to combine all lock releases into one transaction - releaseLocks = atomically . mapM_ takeTMVar + holdLocks = forConcurrently (S.toList keys) $ \key -> atomically $ getPutLock getLock key name + releaseLocks = mapM_ (atomically . takeTMVar) -- getLock and putTMVar can be in one transaction on the assumption that getLock doesn't write in case the lock already exists, -- and in case it is created and added to some shared resource (we use TMap) it also helps avoid contention for the newly created lock. From 2ff5f5a832d092ee453fcbdbb73360cf2f574368 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 24 May 2024 14:13:01 +0100 Subject: [PATCH 063/125] agent: add context to CMD error (#1167) * agent: add context to CMD error * tests, more warnings * fix tests * log TBQueue sizes * log locks * more logs * log sendMessagesB * fix test * log length * refactor * remove logging * revert lock scope change * cleanup * add string error to A_PROHIBITED * remove * remove test limitations * language --- package.yaml | 22 +++- simplexmq.cabal | 14 +-- src/Simplex/FileTransfer/Client.hs | 2 +- src/Simplex/FileTransfer/Client/Main.hs | 6 +- src/Simplex/FileTransfer/Server.hs | 2 +- src/Simplex/Messaging/Agent.hs | 117 +++++++++--------- src/Simplex/Messaging/Agent/Client.hs | 8 +- src/Simplex/Messaging/Agent/Protocol.hs | 32 ++--- src/Simplex/Messaging/Agent/Store/SQLite.hs | 2 +- src/Simplex/Messaging/Crypto/Ratchet.hs | 4 +- .../Messaging/Transport/HTTP2/Client.hs | 2 +- src/Simplex/Messaging/Transport/HTTP2/File.hs | 2 +- src/Simplex/Messaging/Transport/Server.hs | 2 +- tests/AgentTests.hs | 13 +- tests/AgentTests/FunctionalAPITests.hs | 11 +- tests/AgentTests/NotificationTests.hs | 5 +- 16 files changed, 131 insertions(+), 113 deletions(-) diff --git a/package.yaml b/package.yaml index be2286377..8cd924b4e 100644 --- a/package.yaml +++ b/package.yaml @@ -177,13 +177,31 @@ tests: ghc-options: # - -haddock - - -Wall + - -Weverything + - -Wno-missing-exported-signatures + - -Wno-missing-import-lists + - -Wno-missed-specialisations + - -Wno-all-missed-specialisations + - -Wno-unsafe + - -Wno-safe + - -Wno-missing-local-signatures + - -Wno-missing-kind-signatures + - -Wno-missing-deriving-strategies + - -Wno-monomorphism-restriction + - -Wno-prepositive-qualified-module + - -Wno-unused-packages + - -Wno-implicit-prelude + - -Wno-missing-safe-haskell-mode + - -Wno-missing-export-lists + - -Wno-partial-fields - -Wcompat + - -Werror=incomplete-record-updates - -Werror=incomplete-patterns + - -Werror=incomplete-uni-patterns - -Werror=missing-methods + - -Werror=tabs - -Wredundant-constraints - -Wincomplete-record-updates - - -Wincomplete-uni-patterns - -Wunused-type-patterns - -O2 diff --git a/simplexmq.cabal b/simplexmq.cabal index e0fb278a1..c61e68624 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -175,7 +175,7 @@ library src default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 include-dirs: cbits c-sources: @@ -256,7 +256,7 @@ executable ntf-server apps/ntf-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -331,7 +331,7 @@ executable smp-agent apps/smp-agent default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -406,7 +406,7 @@ executable smp-server apps/smp-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -481,7 +481,7 @@ executable xftp apps/xftp default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -556,7 +556,7 @@ executable xftp-server apps/xftp-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -664,7 +664,7 @@ test-suite simplexmq-test tests default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1 + ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1 build-depends: HUnit ==1.6.* , QuickCheck ==2.14.* diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index e4a64c0c9..ff7742e67 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -216,7 +216,7 @@ sendXFTPTransmission XFTPClient {config, thParams, http2Client} t chunkSpec_ = d forM_ chunkSpec_ $ \XFTPChunkSpec {filePath, chunkOffset, chunkSize} -> withFile filePath ReadMode $ \h -> do hSeek h AbsoluteSeek $ fromIntegral chunkOffset - hSendFile h send $ fromIntegral chunkSize + hSendFile h send chunkSize done createXFTPChunk :: diff --git a/src/Simplex/FileTransfer/Client/Main.hs b/src/Simplex/FileTransfer/Client/Main.hs index b3fa494ed..0acc6d3c9 100644 --- a/src/Simplex/FileTransfer/Client/Main.hs +++ b/src/Simplex/FileTransfer/Client/Main.hs @@ -332,7 +332,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re (sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g rKeys <- atomically $ L.fromList <$> replicateM numRecipients (C.generateAuthKeyPair C.SEd25519 g) digest <- liftIO $ getChunkDigest chunkSpec - let ch = FileInfo {sndKey, size = fromIntegral chunkSize, digest} + let ch = FileInfo {sndKey, size = chunkSize, digest} c <- withRetry retryCount $ getXFTPServerClient a xftpServer (sndId, rIds) <- withRetry retryCount $ createXFTPChunk c spKey ch (L.map fst rKeys) auth withReconnect a xftpServer retryCount $ \c' -> uploadXFTPChunk c' spKey sndId chunkSpec @@ -344,7 +344,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re when verbose $ putStrLn "" let recipients = L.toList $ L.map ChunkReplicaId rIds `L.zip` L.map snd rKeys replicas = [SentFileChunkReplica {server = xftpServer, recipients}] - pure (chunkNo, SentFileChunk {chunkNo, sndId, sndPrivateKey = spKey, chunkSize = FileSize $ fromIntegral chunkSize, digest = FileDigest digest, replicas}) + pure (chunkNo, SentFileChunk {chunkNo, sndId, sndPrivateKey = spKey, chunkSize = FileSize chunkSize, digest = FileDigest digest, replicas}) getXFTPServer :: TVar StdGen -> NonEmpty XFTPServerWithAuth -> IO XFTPServerWithAuth getXFTPServer gen = \case srv :| [] -> pure srv @@ -563,7 +563,7 @@ prepareChunkSpecs filePath chunkSizes = reverse . snd $ foldl' addSpec (0, []) c where addSpec :: (Int64, [XFTPChunkSpec]) -> Word32 -> (Int64, [XFTPChunkSpec]) addSpec (chunkOffset, specs) sz = - let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = fromIntegral sz} + let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = sz} in (chunkOffset + fromIntegral sz, spec : specs) getEncPath :: MonadIO m => Maybe FilePath -> String -> m FilePath diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 41c652ea2..ea18b4fdc 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -326,7 +326,7 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea send $ byteString t -- timeout sending file in the same way as receiving forM_ serverFile_ $ \ServerFile {filePath, fileSize, sbState} -> do - withFile filePath ReadMode $ \h -> sendEncFile h send sbState (fromIntegral fileSize) + withFile filePath ReadMode $ \h -> sendEncFile h send sbState fileSize done data VerificationResult = VRVerified XFTPRequest | VRFailed diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 1ca92b2e1..bb6e52bad 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -120,10 +120,11 @@ module Simplex.Messaging.Agent ) where -import Control.Logger.Simple (logError, logInfo, showText) +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import Data.Bifunctor (bimap, first, second) @@ -571,7 +572,7 @@ withAgentEnv c a = ExceptT $ runExceptT a `runReaderT` agentEnv c logConnection :: AgentClient -> Bool -> IO () logConnection c connected = let event = if connected then "connected to" else "disconnected from" - in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"] + in logInfo $ T.unwords ["client", tshow (clientId c), event, "Agent"] -- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's. runAgentClient :: AgentClient -> AM' () @@ -651,14 +652,14 @@ joinConnAsync c userId corrId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup pure connId Nothing -> throwError $ AGENT A_VERSION joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _subMode _cInfo _pqEncryption = - throwError $ CMD PROHIBITED + throwE $ CMD PROHIBITED "joinConnAsync" allowConnectionAsync' :: AgentClient -> ACorrId -> ConnId -> ConfirmationId -> ConnInfo -> AM () allowConnectionAsync' c corrId connId confId ownConnInfo = withStore c (`getConn` connId) >>= \case SomeConn _ (RcvConnection _ RcvQueue {server}) -> enqueueCommand c corrId connId (Just server) $ AClientCommand $ APC SAEConn $ LET confId ownConnInfo - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "allowConnectionAsync" acceptContactAsync' :: AgentClient -> ACorrId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId acceptContactAsync' c corrId enableNtfs invId ownConnInfo pqSupport subMode = do @@ -669,7 +670,7 @@ acceptContactAsync' c corrId enableNtfs invId ownConnInfo pqSupport subMode = do joinConnAsync c userId corrId enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "acceptContactAsync" ackMessageAsync' :: AgentClient -> ACorrId -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AM () ackMessageAsync' c corrId connId msgId rcptInfo_ = do @@ -678,14 +679,14 @@ ackMessageAsync' c corrId connId msgId rcptInfo_ = do SCDuplex -> enqueueAck SCRcv -> enqueueAck SCSnd -> throwError $ CONN SIMPLEX - SCContact -> throwError $ CMD PROHIBITED - SCNew -> throwError $ CMD PROHIBITED + SCContact -> throwE $ CMD PROHIBITED "ackMessageAsync: SCContact" + SCNew -> throwE $ CMD PROHIBITED "ackMessageAsync: SCNew" where enqueueAck :: AM () enqueueAck = do let mId = InternalId msgId RcvMsg {msgType} <- withStore c $ \db -> getRcvMsg db connId mId - when (isJust rcptInfo_ && msgType /= AM_A_MSG_) $ throwError $ CMD PROHIBITED + when (isJust rcptInfo_ && msgType /= AM_A_MSG_) $ throwE $ CMD PROHIBITED "ackMessageAsync: receipt not allowed" (RcvQueue {server}, _) <- withStore c $ \db -> setMsgUserAck db connId mId enqueueCommand c corrId connId (Just server) . AClientCommand $ APC SAEConn $ ACK msgId rcptInfo_ @@ -713,14 +714,14 @@ switchConnectionAsync' c corrId connId = withConnLock c connId "switchConnectionAsync" $ withStore c (`getConn` connId) >>= \case SomeConn _ (DuplexConnection cData rqs@(rq :| _rqs) sqs) - | isJust (switchingRQ rqs) -> throwError $ CMD PROHIBITED + | isJust (switchingRQ rqs) -> throwE $ CMD PROHIBITED "switchConnectionAsync: already switching" | otherwise -> do - when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED + when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "switchConnectionAsync: send prohibited" rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn SWCH let rqs' = updatedQs rq1 rqs pure . connectionStats $ DuplexConnection cData rqs' sqs - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "switchConnectionAsync: not duplex" newConn :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> AM (ConnId, ConnectionRequestUri c) newConn c userId connId enableNtfs cMode clientData pqInitKeys subMode = @@ -737,7 +738,7 @@ newConnSrv c userId connId hasNewConn enableNtfs cMode clientData pqInitKeys sub newRcvConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv = do case (cMode, pqInitKeys) of - (SCMContact, CR.IKUsePQ) -> throwError $ CMD PROHIBITED + (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e @@ -863,7 +864,7 @@ joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSuppo ExceptT $ updateNewConnSnd db connId q confirmQueueAsync c cData q' srv cInfo (Just e2eSndParams) subMode joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode _pqSupport _srv = do - throwError $ CMD PROHIBITED + throwE $ CMD PROHIBITED "joinConnSrvAsync" createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do @@ -888,7 +889,7 @@ allowConnection' c connId confId ownConnInfo = withConnLock c connId "allowConne liftIO $ setRcvQueueConfirmedE2E db rq dhSecret $ min v v' pure senderKey enqueueCommand c "" connId (Just server) . AInternalCommand $ ICAllowSecure rcvId senderKey - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "allowConnection" -- | Accept contact (ACPT command) in Reader monad acceptContact' :: AgentClient -> ConnId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId @@ -900,7 +901,7 @@ acceptContact' c connId enableNtfs invId ownConnInfo pqSupport subMode = withCon joinConn c userId connId False enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "acceptContact" -- | Reject contact (RJCT command) in Reader monad rejectContact' :: AgentClient -> ConnId -> InvitationId -> AM () @@ -1000,14 +1001,14 @@ resubscribeConnections' c connIds = do getConnectionMessage' :: AgentClient -> ConnId -> AM (Maybe SMPMsgMeta) getConnectionMessage' c connId = do - whenM (atomically $ hasActiveSubscription c connId) . throwError $ CMD PROHIBITED + whenM (atomically $ hasActiveSubscription c connId) . throwE $ CMD PROHIBITED "getConnectionMessage: subscribed" SomeConn _ conn <- withStore c (`getConn` connId) case conn of DuplexConnection _ (rq :| _) _ -> getQueueMessage c rq RcvConnection _ rq -> getQueueMessage c rq ContactConnection _ rq -> getQueueMessage c rq SndConnection _ _ -> throwError $ CONN SIMPLEX - NewConnection _ -> throwError $ CMD PROHIBITED + NewConnection _ -> throwE $ CMD PROHIBITED "getConnectionMessage: NewConnection" getNotificationMessage' :: AgentClient -> C.CbNonce -> ByteString -> AM (NotificationInfo, [SMPMsgMeta]) getNotificationMessage' c nonce encNtfInfo = do @@ -1019,7 +1020,7 @@ getNotificationMessage' c nonce encNtfInfo = do ntfMsgMeta <- (eitherToMaybe . smpDecode <$> agentCbDecrypt rcvNtfDhSecret nmsgNonce encNMsgMeta) `catchAgentError` \_ -> pure Nothing maxMsgs <- asks $ ntfMaxMessages . config (NotificationInfo {ntfConnId, ntfTs, ntfMsgMeta},) <$> getNtfMessages ntfConnId ntfMsgMeta maxMsgs - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "getNotificationMessage" where getNtfMessages ntfConnId nMeta = getMsg where @@ -1071,7 +1072,7 @@ sendMessagesB_ c reqs connIds = withConnLocks c connIds "sendMessages" $ do where prepareMsg :: ConnData -> NonEmpty SndQueue -> ([ConnId], Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) prepareMsg cData@ConnData {connId, pqSupport} sqs - | ratchetSyncSendProhibited cData = (acc, Left $ CMD PROHIBITED) + | ratchetSyncSendProhibited cData = (acc, Left $ CMD PROHIBITED "sendMessagesB: send prohibited") -- connection is only updated if PQ encryption was disabled, and now it has to be enabled. -- support for PQ encryption (small message envelopes) will not be disabled when message is sent. | pqEnc == PQEncOn && pqSupport == PQSupportOff = @@ -1133,7 +1134,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do withStore c (`getConn` connId) >>= \case SomeConn _ conn@(DuplexConnection _ (replaced :| _rqs) _) -> switchDuplexConnection c conn replaced >>= notify . SWITCH QDRcv SPStarted - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "SWCH: not duplex" DEL -> withServer' . tryCommand $ deleteConnection' c connId >> notify OK _ -> notify $ ERR $ INTERNAL $ "unsupported async command " <> show (aCommandTag cmd) AInternalCommand cmd -> case cmd of @@ -1479,8 +1480,8 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do DuplexConnection {} -> ack >> sendRcpt conn >> del RcvConnection {} -> ack >> del SndConnection {} -> throwError $ CONN SIMPLEX - ContactConnection {} -> throwError $ CMD PROHIBITED - NewConnection _ -> throwError $ CMD PROHIBITED + ContactConnection {} -> throwE $ CMD PROHIBITED "ackMessage: ContactConnection" + NewConnection _ -> throwE $ CMD PROHIBITED "ackMessage: NewConnection" where ack :: AM () ack = do @@ -1494,7 +1495,7 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do msg@RcvMsg {msgType, msgReceipt} <- withStore c $ \db -> getRcvMsg db connId $ InternalId msgId case rcptInfo_ of Just rcptInfo -> do - unless (msgType == AM_A_MSG_) $ throwError (CMD PROHIBITED) + unless (msgType == AM_A_MSG_) . throwE $ CMD PROHIBITED "ackMessage: receipt not allowed" when (connAgentVersion >= deliveryRcptsSMPAgentVersion) $ do let RcvMsg {msgMeta = MsgMeta {sndMsgId}, internalHash} = msg rcpt = A_RCVD [AMessageReceipt {agentMsgId = sndMsgId, msgHash = internalHash, rcptInfo}] @@ -1510,12 +1511,12 @@ switchConnection' c connId = withConnLock c connId "switchConnection" $ withStore c (`getConn` connId) >>= \case SomeConn _ conn@(DuplexConnection cData rqs@(rq :| _rqs) _) - | isJust (switchingRQ rqs) -> throwError $ CMD PROHIBITED + | isJust (switchingRQ rqs) -> throwE $ CMD PROHIBITED "switchConnection: already switching" | otherwise -> do - when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED + when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "switchConnection: send prohibited" rq' <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted switchDuplexConnection c conn rq' - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "switchConnection: not duplex" switchDuplexConnection :: AgentClient -> Connection 'CDuplex -> RcvQueue -> AM ConnectionStats switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs sqs) rq@RcvQueue {server, dbQueueId = DBQueueId dbQueueId, sndId} = do @@ -1540,7 +1541,7 @@ abortConnectionSwitch' c connId = SomeConn _ (DuplexConnection cData rqs sqs) -> case switchingRQ rqs of Just rq | canAbortRcvSwitch rq -> do - when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED + when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "abortConnectionSwitch: send prohibited" -- multiple queues to which the connections switches were possible when repeating switch was allowed let (delRqs, keepRqs) = L.partition ((Just (dbQId rq) ==) . dbReplaceQId) rqs case L.nonEmpty keepRqs of @@ -1553,9 +1554,9 @@ abortConnectionSwitch' c connId = conn' = DuplexConnection cData rqs'' sqs pure $ connectionStats conn' _ -> throwError $ INTERNAL "won't delete all rcv queues in connection" - | otherwise -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD PROHIBITED + | otherwise -> throwE $ CMD PROHIBITED "abortConnectionSwitch: no rcv queues left" + _ -> throwE $ CMD PROHIBITED "abortConnectionSwitch: not allowed" + _ -> throwE $ CMD PROHIBITED "abortConnectionSwitch: not duplex" synchronizeRatchet' :: AgentClient -> ConnId -> PQSupport -> Bool -> AM ConnectionStats synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchronizeRatchet" $ do @@ -1575,8 +1576,8 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni let cData'' = cData' {ratchetSyncState = RSStarted} :: ConnData conn' = DuplexConnection cData'' rqs sqs pure $ connectionStats conn' - | otherwise -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD PROHIBITED + | otherwise -> throwE $ CMD PROHIBITED "synchronizeRatchet: not allowed" + _ -> throwE $ CMD PROHIBITED "synchronizeRatchet: not duplex" ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM () ackQueueMessage c rq srvMsgId = @@ -1593,7 +1594,7 @@ suspendConnection' c connId = withConnLock c connId "suspendConnection" $ do RcvConnection _ rq -> suspendQueue c rq ContactConnection _ rq -> suspendQueue c rq SndConnection _ _ -> throwError $ CONN SIMPLEX - NewConnection _ -> throwError $ CMD PROHIBITED + NewConnection _ -> throwE $ CMD PROHIBITED "suspendConnection" -- | Delete SMP agent connection (DEL command) in Reader monad -- unlike deleteConnectionAsync, this function does not mark connection as deleted in case of deletion failure @@ -1831,7 +1832,7 @@ registerNtfToken' c suppliedDeviceToken suppliedNtfMode = withStore' c (`createNtfToken` tkn) registerToken tkn pure NTRegistered - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "createToken" registerToken :: NtfToken -> AM () registerToken tkn@NtfToken {ntfPubKey, ntfDhKeys = (pubDhKey, privDhKey)} = do (tknId, srvPubDhKey) <- agentNtfRegisterToken c tkn ntfPubKey pubDhKey @@ -1844,7 +1845,7 @@ verifyNtfToken' :: AgentClient -> DeviceToken -> C.CbNonce -> ByteString -> AM ( verifyNtfToken' c deviceToken nonce code = withStore' c getSavedNtfToken >>= \case Just tkn@NtfToken {deviceToken = savedDeviceToken, ntfTokenId = Just tknId, ntfDhSecret = Just dhSecret, ntfMode} -> do - when (deviceToken /= savedDeviceToken) . throwError $ CMD PROHIBITED + when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "verifyNtfToken: different token" code' <- liftEither . bimap cryptoError NtfRegCode $ C.cbDecrypt dhSecret nonce code toStatus <- withToken c tkn (Just (NTConfirmed, NTAVerify code')) (NTActive, Just NTACheck) $ @@ -1853,36 +1854,36 @@ verifyNtfToken' c deviceToken nonce code = cron <- asks $ ntfCron . config agentNtfEnableCron c tknId tkn cron when (ntfMode == NMInstant) $ initializeNtfSubs c - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "verifyNtfToken: no token" checkNtfToken' :: AgentClient -> DeviceToken -> AM NtfTknStatus checkNtfToken' c deviceToken = withStore' c getSavedNtfToken >>= \case Just tkn@NtfToken {deviceToken = savedDeviceToken, ntfTokenId = Just tknId} -> do - when (deviceToken /= savedDeviceToken) . throwError $ CMD PROHIBITED + when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "checkNtfToken: different token" agentNtfCheckToken c tknId tkn - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "checkNtfToken: no token" deleteNtfToken' :: AgentClient -> DeviceToken -> AM () deleteNtfToken' c deviceToken = withStore' c getSavedNtfToken >>= \case Just tkn@NtfToken {deviceToken = savedDeviceToken} -> do - when (deviceToken /= savedDeviceToken) . throwError $ CMD PROHIBITED + when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "deleteNtfToken: different token" deleteToken_ c tkn deleteNtfSubs c NSCSmpDelete - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "deleteNtfToken: no token" getNtfToken' :: AgentClient -> AM (DeviceToken, NtfTknStatus, NotificationsMode, NtfServer) getNtfToken' c = withStore' c getSavedNtfToken >>= \case Just NtfToken {deviceToken, ntfTknStatus, ntfMode, ntfServer} -> pure (deviceToken, ntfTknStatus, ntfMode, ntfServer) - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "getNtfToken" getNtfTokenData' :: AgentClient -> AM NtfToken getNtfTokenData' c = withStore' c getSavedNtfToken >>= \case Just tkn -> pure tkn - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "getNtfTokenData" -- | Set connection notifications, in Reader monad toggleConnectionNtfs' :: AgentClient -> ConnId -> Bool -> AM () @@ -2172,7 +2173,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) smpConfirmation srvMsgId conn senderKey e2ePubKey e2eEncryption_ encConnInfo phVer agentVersion >> ack (SMP.PHEmpty, AgentInvitation {connReq, connInfo}) -> smpInvitation srvMsgId conn connReq connInfo >> ack - _ -> prohibited >> ack + _ -> prohibited "handshake: incorrect state" >> ack (Just e2eDh, Nothing) -> do decryptClientMessage e2eDh clientMsg >>= \case (SMP.PHEmpty, AgentRatchetKey {agentVersion, e2eEncryption}) -> do @@ -2196,7 +2197,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let encryptedMsgHash = C.sha256Hash encAgentMessage g <- asks random atomically updateTotalMsgCount - tryError (agentClientMsg g encryptedMsgHash) >>= \case + tryAgentError (agentClientMsg g encryptedMsgHash) >>= \case Right (Just (msgId, msgMeta, aMessage, rcPrev)) -> do conn'' <- resetRatchetSync case aMessage of @@ -2227,13 +2228,13 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) withStore' c $ \db -> setConnRatchetSync db connId RSOk pure conn'' | otherwise = pure conn' - Right _ -> prohibited >> ack + Right Nothing -> prohibited "msg: bad agent msg" >> ack Left e@(AGENT A_DUPLICATE) -> do atomically updateDupMsgCount withStore' c (\db -> getLastMsg db connId srvMsgId) >>= \case Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck} | userAck -> ackDel internalId - | otherwise -> do + | otherwise -> liftEither (parse smpP (AGENT A_MESSAGE) agentMsgBody) >>= \case AgentMessage _ (A_MSG body) -> do logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId @@ -2292,8 +2293,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) liftIO $ createRcvMsg db connId rq rcvMsg pure $ Just (internalId, msgMeta, aMessage, rc) _ -> pure Nothing - _ -> prohibited >> ack - _ -> prohibited >> ack + _ -> prohibited "msg: bad client msg" >> ack + _ -> prohibited "msg: no keys" >> ack updateConnVersion :: Connection c -> ConnData -> VersionSMPA -> AM (Connection c) updateConnVersion conn' cData' msgAgentVersion = do aVRange <- asks $ smpAgentVRange . config @@ -2330,8 +2331,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) notify :: forall e m. (AEntityI e, MonadIO m) => ACommand 'Agent e -> m () notify = notify' connId - prohibited :: AM () - prohibited = notify . ERR $ AGENT A_PROHIBITED + prohibited :: String -> AM () + prohibited = notify . ERR . AGENT . A_PROHIBITED enqueueCmd :: InternalCommand -> AM () enqueueCmd = enqueueCommand c "" connId (Just srv) . AInternalCommand @@ -2383,7 +2384,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) parseMessage agentMsgBody >>= \case AgentConnInfoReply smpQueues connInfo -> processConf connInfo SMPConfirmation {senderKey, e2ePubKey, connInfo, smpReplyQueues = L.toList smpQueues, smpClientVersion} - _ -> prohibited -- including AgentConnInfo, that is prohibited here in v2 + _ -> prohibited "conf: not AgentConnInfoReply" -- including AgentConnInfo, that is prohibited here in v2 where processConf connInfo senderConf = do let newConfirmation = NewConfirmation {connId, senderConf, ratchetState = rc'} @@ -2393,7 +2394,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) createConfirmation db g newConfirmation let srvs = map qServer $ smpReplyQueues senderConf notify $ CONF confId pqSupport' srvs connInfo - _ -> prohibited + _ -> prohibited "conf: decrypt error or skipped" -- party accepting connection (DuplexConnection _ (RcvQueue {smpClientVersion = v'} :| _) _, Nothing) -> do g <- asks random @@ -2403,15 +2404,15 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let dhSecret = C.dh' e2ePubKey e2ePrivKey withStore' c $ \db -> setRcvQueueConfirmedE2E db rq dhSecret $ min v' smpClientVersion enqueueCmd $ ICDuplexSecure rId senderKey - _ -> prohibited - _ -> prohibited - _ -> prohibited + _ -> prohibited "conf: not AgentConnInfo" + _ -> prohibited "conf: incorrect state" + _ -> prohibited "conf: status /= new" helloMsg :: SMP.MsgId -> MsgMeta -> Connection c -> AM () helloMsg srvMsgId MsgMeta {pqEncryption} conn' = do logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId case status of - Active -> prohibited + Active -> prohibited "hello: active" _ -> case conn' of DuplexConnection _ _ (sq@SndQueue {status = sndStatus} :| _) @@ -2453,7 +2454,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let sndMsgId = InternalSndId agentMsgId SndMsg {internalId = InternalId msgId, msgType, internalHash, msgReceipt} <- withStore c $ \db -> getSndMsgViaRcpt db connId sndMsgId if msgType /= AM_A_MSG_ - then notify (ERR $ AGENT A_PROHIBITED) $> Nothing -- unexpected message type for receipt + then prohibited "receipt: not a msg" $> Nothing else case msgReceipt of Just MsgReceipt {msgRcptStatus = MROk} -> pure Nothing -- already notified with MROk status _ -> do @@ -2561,7 +2562,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) invId <- withStore c $ \db -> createInvitation db g newInv let srvs = L.map qServer $ crSmpQueues crData notify $ REQ invId pqSupport srvs cInfo - _ -> prohibited + _ -> prohibited "inv: sent to message conn" where pqSupported (_, Compatible (CR.E2ERatchetParams v _ _ _), Compatible agentVersion) = PQSupportOn `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just v) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 0d0af894f..37f659c9f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1327,7 +1327,7 @@ subscribeQueues c qs = do where checkQueue rq = do prohibited <- atomically $ hasGetLock c rq - pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED) else Right rq + pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED "subscribeQueues") else Right rq subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) subscribeQueues_ env session smp qs' = do rs <- sendBatch subscribeSMPQueues smp qs' @@ -1595,7 +1595,7 @@ agentXFTPNewChunk :: AgentClient -> SndFileChunk -> Int -> XFTPServerWithAuth -> agentXFTPNewChunk c SndFileChunk {userId, chunkSpec = XFTPChunkSpec {chunkSize}, digest = FileDigest chunkDigest} n (ProtoServerWithAuth srv auth) = do rKeys <- xftpRcvKeys n (sndKey, replicaKey) <- atomically . C.generateAuthKeyPair C.SEd25519 =<< asks random - let fileInfo = FileInfo {sndKey, size = fromIntegral chunkSize, digest = chunkDigest} + let fileInfo = FileInfo {sndKey, size = chunkSize, digest = chunkDigest} logServer "-->" c srv "" "FNEW" tSess <- liftIO $ mkTransportSession c userId srv chunkDigest (sndId, rIds) <- withClient c tSess "FNEW" $ \xftp -> X.createXFTPChunk xftp replicaKey fileInfo (L.map fst rKeys) auth @@ -1658,7 +1658,7 @@ agentCbDecrypt dhSecret nonce msg = cryptoError :: C.CryptoError -> AgentErrorType cryptoError = \case - C.CryptoLargeMsgError -> CMD LARGE + C.CryptoLargeMsgError -> CMD LARGE "CryptoLargeMsgError" C.CryptoHeaderError _ -> AGENT A_MESSAGE -- parsing error C.CERatchetDuplicateMessage -> AGENT A_DUPLICATE C.AESDecryptError -> c DECRYPT_AES @@ -1801,7 +1801,7 @@ storeError = \case SEConnDuplicate -> CONN DUPLICATE SEBadConnType CRcv -> CONN SIMPLEX SEBadConnType CSnd -> CONN SIMPLEX - SEInvitationNotFound -> CMD PROHIBITED + SEInvitationNotFound -> CMD PROHIBITED "SEInvitationNotFound" -- this error is never reported as store error, -- it is used to wrap agent operations when "transaction-like" store access is needed -- NOTE: network IO should NOT be used inside AgentStoreMonad diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 1fc984ac5..0b40edee9 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -157,8 +157,8 @@ where import Control.Applicative (optional, (<|>)) import Control.Monad (unless) -import Control.Monad.Except (runExceptT, throwError) import Control.Monad.IO.Class +import Control.Monad.Trans.Except import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.TH as J import Data.Attoparsec.ByteString.Char8 (Parser) @@ -1471,7 +1471,7 @@ instance StrEncoding MsgErrorType where -- | Error type used in errors sent to agent clients. data AgentErrorType = -- | command or response error - CMD {cmdErr :: CommandErrorType} + CMD {cmdErr :: CommandErrorType, errContext :: String} | -- | connection errors CONN {connErr :: ConnectionErrorType} | -- | SMP protocol errors forwarded to agent clients @@ -1529,7 +1529,7 @@ data SMPAgentError = -- | client or agent message that failed to parse A_MESSAGE | -- | prohibited SMP/agent message - A_PROHIBITED + A_PROHIBITED {prohibitedErr :: String} | -- | incompatible version of SMP client, agent or encryption protocols A_VERSION | -- | cannot decrypt message @@ -1578,7 +1578,7 @@ instance StrEncoding AgentErrorType where strP = A.takeTill (== ' ') >>= \case - "CMD" -> CMD <$> (A.space *> parseRead1) + "CMD" -> CMD <$> (A.space *> parseRead1) <*> (A.space *> textP) "CONN" -> CONN <$> (A.space *> parseRead1) "SMP" -> SMP <$> (A.space *> srvP) <*> _strP "NTF" -> NTF <$> (A.space *> srvP) <*> _strP @@ -1593,9 +1593,8 @@ instance StrEncoding AgentErrorType where _ -> fail "bad AgentErrorType" where srvP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') - textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString strEncode = \case - CMD e -> "CMD " <> bshow e + CMD e cxt -> "CMD " <> bshow e <> " " <> text cxt CONN e -> "CONN " <> bshow e SMP srv e -> "SMP " <> text srv <> " " <> strEncode e NTF srv e -> "NTF " <> text srv <> " " <> strEncode e @@ -1615,20 +1614,23 @@ instance StrEncoding SMPAgentError where A.takeTill (== ' ') >>= \case "MESSAGE" -> pure A_MESSAGE - "PROHIBITED" -> pure A_PROHIBITED + "PROHIBITED" -> A_PROHIBITED <$> (A.space *> textP) "VERSION" -> pure A_VERSION "CRYPTO" -> A_CRYPTO <$> _strP "DUPLICATE" -> pure A_DUPLICATE - "QUEUE" -> A_QUEUE . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + "QUEUE" -> A_QUEUE <$> (A.space *> textP) _ -> fail "bad SMPAgentError" strEncode = \case A_MESSAGE -> "MESSAGE" - A_PROHIBITED -> "PROHIBITED" + A_PROHIBITED e -> "PROHIBITED " <> encodeUtf8 (T.pack e) A_VERSION -> "VERSION" A_CRYPTO e -> "CRYPTO " <> strEncode e A_DUPLICATE -> "DUPLICATE" A_QUEUE e -> "QUEUE " <> encodeUtf8 (T.pack e) +textP :: Parser String +textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString + cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState cryptoErrToSyncState = \case DECRYPT_AES -> RSAllowed @@ -1841,7 +1843,7 @@ commandP binaryP = sd : rds -> SFDONE <$> strDecode (encodeUtf8 sd) <*> mapM (strDecode . encodeUtf8) rds parseCommand :: ByteString -> Either AgentErrorType ACmd -parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX +parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX "parseCommand" -- | Serialize SMP agent command. serializeCommand :: ACommand p e -> ByteString @@ -1931,7 +1933,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody fromParty :: ACmd -> Either AgentErrorType (APartyCmd p) fromParty (ACmd (p :: p1) e cmd) = case testEquality party p of Just Refl -> Right $ APC e cmd - _ -> Left $ CMD PROHIBITED + _ -> Left $ CMD PROHIBITED "fromParty" tConnId :: ARawTransmission -> APartyCmd p -> Either AgentErrorType (APartyCmd p) tConnId (_, entId, _) (APC e cmd) = @@ -1949,7 +1951,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody SUSPENDED {} -> Right cmd -- other responses must have connection ID _ - | B.null entId -> Left $ CMD NO_CONN + | B.null entId -> Left $ CMD NO_CONN "tConnId" | otherwise -> Right cmd cmdWithMsgBody :: APartyCmd p -> IO (Either AgentErrorType (APartyCmd p)) @@ -1972,11 +1974,11 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody str -> case readMaybe str :: Maybe Int of Just size -> runExceptT $ do body <- liftIO $ cGet h size - unless (B.length body == size) $ throwError $ CMD SIZE + unless (B.length body == size) $ throwE $ CMD SIZE "getBody" s <- liftIO $ getLn h - unless (B.null s) $ throwError $ CMD SIZE + unless (B.null s) $ throwE $ CMD SIZE "getBody" pure body - Nothing -> return . Left $ CMD SYNTAX + Nothing -> pure . Left $ CMD SYNTAX "getBody" $(J.deriveJSON defaultJSON ''RcvQueueInfo) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 04dd826a6..6c2c5906d 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -2284,7 +2284,7 @@ createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId pure rcvFileEntityId -createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId) +createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId) createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect" createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile approvedRelays = runExceptT $ do (dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing approvedRelays diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index db13fddc1..14f567820 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -166,9 +166,9 @@ instance TestEquality SRatchetKEMState where class RatchetKEMStateI (s :: RatchetKEMState) where sRatchetKEMState :: SRatchetKEMState s -instance RatchetKEMStateI RKSProposed where sRatchetKEMState = SRKSProposed +instance RatchetKEMStateI 'RKSProposed where sRatchetKEMState = SRKSProposed -instance RatchetKEMStateI RKSAccepted where sRatchetKEMState = SRKSAccepted +instance RatchetKEMStateI 'RKSAccepted where sRatchetKEMState = SRKSAccepted checkRatchetKEMState :: forall t s s' a. (RatchetKEMStateI s, RatchetKEMStateI s') => t s' a -> Either String (t s a) checkRatchetKEMState x = case testEquality (sRatchetKEMState @s) (sRatchetKEMState @s') of diff --git a/src/Simplex/Messaging/Transport/HTTP2/Client.hs b/src/Simplex/Messaging/Transport/HTTP2/Client.hs index d701d4114..71757ca6d 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/Client.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/Client.hs @@ -14,6 +14,7 @@ import Control.Monad import Data.ByteString.Char8 (ByteString) import Data.Functor (($>)) import Data.Time (UTCTime, getCurrentTime) +import qualified Data.X509 as X import qualified Data.X509.CertificateStore as XS import Network.HPACK (BufferSize) import Network.HTTP2.Client (ClientConfig (..), Request, Response) @@ -29,7 +30,6 @@ import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM import UnliftIO.Timeout -import qualified Data.X509 as X data HTTP2Client = HTTP2Client { action :: Maybe (Async HTTP2Response), diff --git a/src/Simplex/Messaging/Transport/HTTP2/File.hs b/src/Simplex/Messaging/Transport/HTTP2/File.hs index 10238f161..aef98acaa 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/File.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/File.hs @@ -23,7 +23,7 @@ hReceiveFile getBody h size = get $ fromIntegral size if | chSize > sz -> pure (chSize - sz) | chSize > 0 -> B.hPut h ch >> get (sz - chSize) - | otherwise -> pure (-fromIntegral sz) + | otherwise -> pure (-sz) hSendFile :: Handle -> (Builder -> IO ()) -> Word32 -> IO () hSendFile h send = go diff --git a/src/Simplex/Messaging/Transport/Server.hs b/src/Simplex/Messaging/Transport/Server.hs index 145b438e0..495ad76bb 100644 --- a/src/Simplex/Messaging/Transport/Server.hs +++ b/src/Simplex/Messaging/Transport/Server.hs @@ -118,7 +118,7 @@ runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket serve let closeConn _ = do atomically $ modifyTVar' clients $ IM.delete cId gracefulClose conn 5000 `catchAll_` pure () -- catchAll_ is needed here in case the connection was closed earlier - atomically $ modifyTVar' gracefullyClosed (+1) + atomically $ modifyTVar' gracefullyClosed (+ 1) tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn atomically $ modifyTVar' clients $ IM.insert cId tId diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 7a3077532..186c91e13 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -7,7 +7,6 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests (agentTests) where @@ -358,7 +357,7 @@ testRejectContactRequest _ alice bob = do -- RJCT must use correct contact connection alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND) alice #: ("2b", "a_contact", "RJCT " <> aInvId) #> ("2b", "a_contact", OK) - alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") #> ("3", "bob", ERR $ A.CMD PROHIBITED) + alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") =#> \case ("3", "bob", ERR (A.CMD PROHIBITED _)) -> True; _ -> False bob #:# "nothing should be delivered to bob" testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () @@ -430,8 +429,8 @@ testServerConnectionAfterError t _ = do withAgent1 $ \bob -> do withAgent2 $ \alice -> do - bob #:! ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT - alice #:! ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT + bob #:! ("1", "alice", "SUB") =#> \case ("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT; _ -> False + alice #:! ("1", "bob", "SUB") =#> \case ("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT; _ -> False withServer $ do alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False @@ -613,12 +612,12 @@ sampleDhKey = "MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" syntaxTests :: forall c. Transport c => TProxy c -> Spec syntaxTests t = do - it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX") + it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX parseCommand") describe "NEW" $ do describe "valid" $ do it "with correct parameter" $ ("211", "", "NEW T INV subscribe") >#>= \case ("211", _, "INV" : _) -> True; _ -> False describe "invalid" $ do - it "with incorrect parameter" $ ("222", "", "NEW T hi subscribe") >#> ("222", "", "ERR CMD SYNTAX") + it "with incorrect parameter" $ ("222", "", "NEW T hi subscribe") >#> ("222", "", "ERR CMD SYNTAX parseCommand") describe "JOIN" $ do describe "valid" $ do @@ -636,7 +635,7 @@ syntaxTests t = do ) >#> ("311", "a", "ERR SMP smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001 AUTH") describe "invalid" $ do - it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX") + it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX parseCommand") where -- simple test for one command with the expected response (>#>) :: ARawTransmission -> ARawTransmission -> Expectation diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 6e5e09fc3..250e95474 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -14,7 +14,6 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.FunctionalAPITests ( functionalAPITests, @@ -575,13 +574,13 @@ testEnablePQEncryption = -- switched to smaller envelopes (before reporting PQ encryption enabled) sml <- largeMsg g PQSupportOn -- fail because of message size - Left (A.CMD LARGE) <- tryError $ A.sendMessage ca bId PQEncOn SMP.noMsgFlags lrg + Left (A.CMD LARGE _) <- tryError $ A.sendMessage ca bId PQEncOn SMP.noMsgFlags lrg (9, PQEncOff) <- A.sendMessage ca bId PQEncOn SMP.noMsgFlags sml get ca =##> \case ("", connId, SENT 9) -> connId == bId; _ -> False get cb =##> \case ("", connId, MsgErr' 8 MsgSkipped {} PQEncOff msg') -> connId == aId && msg' == sml; _ -> False ackMessage cb aId 8 Nothing -- -- fail in reply to sync IDss - Left (A.CMD LARGE) <- tryError $ A.sendMessage cb aId PQEncOn SMP.noMsgFlags lrg + Left (A.CMD LARGE _) <- tryError $ A.sendMessage cb aId PQEncOn SMP.noMsgFlags lrg (10, PQEncOff) <- A.sendMessage cb aId PQEncOn SMP.noMsgFlags sml get cb =##> \case ("", connId, SENT 10) -> connId == aId; _ -> False get ca =##> \case ("", connId, MsgErr' 10 MsgSkipped {} PQEncOff msg') -> connId == bId && msg' == sml; _ -> False @@ -608,8 +607,8 @@ testEnablePQEncryption = (b, 26, sml) \#>\ a (a, 27, sml) \#>\ b -- PQ encryption is now disabled, but support remained enabled, so we still cannot send larger messages - Left (A.CMD LARGE) <- tryError $ A.sendMessage ca bId PQEncOff SMP.noMsgFlags (sml <> "123456") - Left (A.CMD LARGE) <- tryError $ A.sendMessage cb aId PQEncOff SMP.noMsgFlags (sml <> "123456") + Left (A.CMD LARGE _) <- tryError $ A.sendMessage ca bId PQEncOff SMP.noMsgFlags (sml <> "123456") + Left (A.CMD LARGE _) <- tryError $ A.sendMessage cb aId PQEncOff SMP.noMsgFlags (sml <> "123456") pure () where (\#>\) = PQEncOff `sndRcv` PQEncOff @@ -2480,7 +2479,7 @@ testDeliveryReceipts = get a =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False ackMessage a bId 6 $ Just "" get b =##> \case ("", c, Rcvd 6) -> c == aId; _ -> False - ackMessage b aId 7 (Just "") `catchError` \e -> liftIO $ e `shouldBe` A.CMD PROHIBITED + ackMessage b aId 7 (Just "") `catchError` \case (A.CMD PROHIBITED _) -> pure (); e -> liftIO $ expectationFailure ("unexpected error " <> show e) ackMessage b aId 7 Nothing testDeliveryReceiptsVersion :: HasCallStack => ATransport -> IO () diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 348694f7e..a7c3fb25a 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -7,7 +7,6 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.NotificationTests where @@ -182,7 +181,7 @@ testNotificationToken APNSMockServer {apnsQ} = do NTActive <- checkNtfToken a tkn deleteNtfToken a tkn -- agent deleted this token - Left (CMD PROHIBITED) <- tryE $ checkNtfToken a tkn + Left (CMD PROHIBITED _) <- tryE $ checkNtfToken a tkn pure () (.->) :: J.Value -> J.Key -> ExceptT AgentErrorType IO ByteString @@ -375,7 +374,7 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@Agen pure (bobId, aliceId, nonce, message) -- alice client already has subscription for the connection - Left (CMD PROHIBITED) <- runExceptT $ getNotificationMessage alice nonce message + Left (CMD PROHIBITED _) <- runExceptT $ getNotificationMessage alice nonce message -- aliceNtf client doesn't have subscription and is allowed to get notification message withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do From 470dc7439183589ab24d530384cff88494895e92 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 24 May 2024 14:25:05 +0100 Subject: [PATCH 064/125] ntf server: do not persist server connection errors --- src/Simplex/Messaging/Notifications/Server/Main.hs | 2 +- tests/NtfClient.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index b28dd6111..2d2f0bfc4 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -113,7 +113,7 @@ ntfServerCLI cfgPath logPath = clientQSize = 64, subQSize = 512, pushQSize = 1048, - smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 15}, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 0}, apnsConfig = defaultAPNSPushClientConfig, subsBatchSize = 900, inactiveClientExpiration = diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index f5b8a4b2e..bd8cee771 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -89,7 +89,7 @@ ntfServerCfg = clientQSize = 1, subQSize = 1, pushQSize = 1, - smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 1}, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 0}, apnsConfig = defaultAPNSPushClientConfig { apnsPort = apnsTestPort, From bd67844169d2206d8543c01e6ed966315115b0e3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 24 May 2024 14:26:02 +0100 Subject: [PATCH 065/125] 5.8.0.5 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 8cd924b4e..61f144eb1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.4 +version: 5.8.0.5 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index c61e68624..55d8cd637 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.4 +version: 5.8.0.5 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From df35c50b99a1291aac3ff86ed71387bb4f03c984 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 25 May 2024 11:10:14 +0100 Subject: [PATCH 066/125] 5.7.5.0 --- CHANGELOG.md | 5 +++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a7bb5b3..8b521e0e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 5.7.5 + +SMP agent: +- fail if non-unique connection IDs are passed to sendMessages (to prevent client errors and deadlocks). + # 5.7.4 SMP agent: diff --git a/package.yaml b/package.yaml index a1f4a225a..49dc27c0c 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.4.1 +version: 5.7.5.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 9bfa146c1..2e7d7bb53 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.4.1 +version: 5.7.5.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From ab7b350521ed732b1c9b686ebe9441ba30f30a91 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 27 May 2024 14:55:04 +0100 Subject: [PATCH 067/125] agent: prevent sending not-batched client commands once requests time out (#1173) --- src/Simplex/Messaging/Client.hs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index ecf4ee766..e33c91873 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -154,7 +154,7 @@ data PClient v err msg = PClient timeoutErrorCount :: TVar Int, clientCorrId :: TVar ChaChaDRG, sentCommands :: TMap CorrId (Request err msg), - sndQ :: TBQueue ByteString, + sndQ :: TBQueue (Maybe (TVar Bool), ByteString), rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)), msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg)) } @@ -474,7 +474,11 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize `finally` disconnected c' send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () - send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= void . tPutLog h + send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= sendPending + where + sendPending (Nothing, s) = send_ s + sendPending (Just pending, s) = whenM (readTVarIO pending) $ send_ s + send_ = void . tPutLog h receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () receive ProtocolClient {client_ = PClient {rcvQ, lastReceived, timeoutErrorCount}} h = forever $ do @@ -965,29 +969,33 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do pure [Response entityId $ Left $ PCETransportError e] TBTransmissions s n rs | n > 0 -> do - atomically $ writeTBQueue sndQ s + atomically $ writeTBQueue sndQ (Nothing, s) -- do not expire batched responses mapConcurrently (getResponse c Nothing) rs | otherwise -> pure [] TBTransmission s r -> do - atomically $ writeTBQueue sndQ s + atomically $ writeTBQueue sndQ (Nothing, s) (: []) <$> getResponse c Nothing r -- | Send Protocol command sendProtocolCommand :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg sendProtocolCommand c = sendProtocolCommand_ c Nothing Nothing +-- Currently there is coupling - batch commands do not expire, and individually sent commands do. +-- This is to reflect the fact that we send subscriptions only as batches, and also because we do not track a separate timeout for the whole batch, so it is not obvious when should we expire it. +-- We could expire a batch of deletes, for example, either when the first response expires or when the last one does. +-- But a better solution is to process delayed delete responses. sendProtocolCommand_ :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe Int -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ tOut pKey entId cmd = ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd) where -- two separate "atomically" needed to avoid blocking sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg) - sendRecv t_ r = case t_ of + sendRecv t_ r@Request {pending} = case t_ of Left e -> pure . Left $ PCETransportError e Right t | B.length s > blockSize - 2 -> pure . Left $ PCETransportError TELargeMsg | otherwise -> do - atomically $ writeTBQueue sndQ s + atomically $ writeTBQueue sndQ (Just pending, s) response <$> getResponse c tOut r where s From c8b2bb2ae1fe0e7dede6e57ba5807e062604f6a4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 28 May 2024 08:35:43 +0100 Subject: [PATCH 068/125] agent: process message sent in expired response to ACK (it will increase duplicates count) (#1175) --- src/Simplex/Messaging/Agent.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index bb6e52bad..6f0f52589 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -2102,7 +2102,6 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) Left e -> lift $ notifyErr connId e STResponse (Cmd SRecipient cmd) respOrErr -> withRcvConn entId $ \rq conn -> case cmd of - -- TODO process expired responses to ACK and DEL SMP.SUB -> case respOrErr of Right SMP.OK -> processSubOk rq upConnIds Right msg@SMP.MSG {} -> do @@ -2110,7 +2109,10 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) processSMP rq conn (toConnData conn) msg Right r -> processSubErr rq $ unexpectedResponse r Left e -> unless (temporaryClientError e) $ processSubErr rq e -- timeout/network was already reported - _ -> pure () + SMP.ACK _ -> case respOrErr of + Right msg@SMP.MSG {} -> processSMP rq conn (toConnData conn) msg + _ -> pure () -- TODO process OK response to ACK + _ -> pure () -- TODO process expired response to DEL STResponse {} -> pure () -- TODO process expired responses to sent messages STUnexpectedError e -> do logServer "<--" c srv entId $ "error: " <> bshow e From 4a96dbf871dd28cf22a0a06b2e29cf45d1a439c7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 28 May 2024 09:38:47 +0100 Subject: [PATCH 069/125] server: preprocess proxy commands that will not be connecting to network to reduce concurrency, do not wait for destination relay responses before processing the next command (#1174) * server: preprocess proxy commands that will not be connecting to network to reduce concurrency * implementation * tests * increase proxy client concurrency * simplify * refactor * refactor2 * rename * refactor3 * fix 8.10.7 --- src/Simplex/Messaging/Client/Agent.hs | 16 ++- src/Simplex/Messaging/Server.hs | 144 ++++++++++++------------ src/Simplex/Messaging/Server/Env/STM.hs | 2 +- tests/AgentTests/EqInstances.hs | 5 + tests/SMPProxyTests.hs | 4 + 5 files changed, 99 insertions(+), 72 deletions(-) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index bd878ada7..3b4a78cf5 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -45,7 +45,7 @@ import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport -import Simplex.Messaging.Util (catchAll_, ifM, toChunks, whenM, ($>>=)) +import Simplex.Messaging.Util (catchAll_, ifM, toChunks, whenM, ($>>=), (<$$>)) import System.Timeout (timeout) import UnliftIO (async) import qualified UnliftIO.Exception as E @@ -287,6 +287,20 @@ notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m () notify ca evt = atomically $ writeTBQueue (agentQ ca) evt {-# INLINE notify #-} +-- Returns already connected client for proxying messages or Nothing if client is absent, not connected yet or stores expired error. +-- If Nothing is return proxy will spawn a new thread to wait or to create another client connection to destination relay. +getConnectedSMPServerClient :: SMPClientAgent -> SMPServer -> IO (Maybe (Either SMPClientError (OwnServer, SMPClient))) +getConnectedSMPServerClient SMPClientAgent {smpClients} srv = + atomically (TM.lookup srv smpClients $>>= \v -> (v,) <$$> tryReadTMVar (sessionVar v)) -- Nothing: client is absent or not connected yet + $>>= \case + (_, Right r) -> pure $ Just $ Right r + (v, Left (e, ts_)) -> + pure ts_ $>>= \ts -> -- proxy will create a new connection if ts_ is Nothing + ifM + ((ts <) <$> liftIO getCurrentTime) -- error persistence interval period expired? + (Nothing <$ atomically (removeSessVar v srv smpClients)) -- proxy will create a new connection + (pure $ Just $ Left e) -- not expired, returning error + lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe (OwnServer, SMPClient)) lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index c0ab7df8e..adf0b5df7 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -59,7 +59,7 @@ import Data.List (intercalate, mapAccumR) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M -import Data.Maybe (isNothing) +import Data.Maybe (catMaybes, fromMaybe, isNothing) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) @@ -70,8 +70,8 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError, temporaryClientError) -import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient) +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, forwardSMPMessage, smpProxyError, temporaryClientError) +import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient, getConnectedSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -103,7 +103,6 @@ import UnliftIO.IO import UnliftIO.STM #if MIN_VERSION_base(4,18,0) import Data.List (sort) -import Data.Maybe (fromMaybe) import GHC.Conc (listThreads, threadStatus) import GHC.Conc.Sync (threadLabel) #endif @@ -657,40 +656,33 @@ forkClient Client {endThreads, endThreadSeq} label action = do client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M () client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId, procThreads} Server {subscribedQ, ntfSubscribedQ, notifiers} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands" - forever $ do - (proxied, rs) <- partitionEithers . L.toList <$> (mapM processCommand =<< atomically (readTBQueue rcvQ)) - forM_ (L.nonEmpty rs) reply - forM_ (L.nonEmpty proxied) $ \cmds -> mapM forkProxiedCmd cmds >>= mapM (atomically . takeTMVar) >>= reply + forever $ + atomically (readTBQueue rcvQ) + >>= mapM processCommand + >>= mapM_ reply . L.nonEmpty . catMaybes . L.toList where reply :: MonadIO m => NonEmpty (Transmission BrokerMsg) -> m () reply = atomically . writeTBQueue sndQ - forkProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (TMVar (Transmission BrokerMsg)) - forkProxiedCmd cmd = do - res <- newEmptyTMVarIO - bracket_ wait signal . forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " proxy") $ - -- commands MUST be processed under a reasonable timeout or the client would halt - processProxiedCmd cmd >>= atomically . putTMVar res - pure res - where - wait = do - ServerConfig {serverClientConcurrency} <- asks config - atomically $ do - used <- readTVar procThreads - when (used >= serverClientConcurrency) retry - writeTVar procThreads $! used + 1 - signal = atomically $ modifyTVar' procThreads (\t -> t - 1) - processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg) - processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of - PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR $ PROXY BASIC_AUTH) + processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Maybe (Transmission BrokerMsg)) + processProxiedCmd (corrId, sessId, command) = (corrId,sessId,) <$$> case command of + PRXY srv auth -> ifM allowProxy getRelay (pure $ Just $ ERR $ PROXY BASIC_AUTH) where allowProxy = do ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth getRelay = do + ProxyAgent {smpAgent = a} <- asks proxyAgent + liftIO (getConnectedSMPServerClient a srv) >>= \case + Just r -> Just <$> proxyServerResponse a r + Nothing -> + forkProxiedCmd $ + liftIO (runExceptT (getSMPServerClient'' a srv) `catch` (pure . Left . PCEIOError)) + >>= proxyServerResponse a + proxyServerResponse :: SMPClientAgent -> Either SMPClientError (OwnServer, SMPClient) -> M BrokerMsg + proxyServerResponse a smp_ = do ServerStats {pRelays, pRelaysOwn} <- asks serverStats let inc = mkIncProxyStats pRelays pRelaysOwn - ProxyAgent {smpAgent = a} <- asks proxyAgent - liftIO (runExceptT (getSMPServerClient'' a srv) `catch` (pure . Left . PCEIOError)) >>= \case + case smp_ of Right (own, smp) -> do inc own pRequests case proxyResp smp of @@ -704,7 +696,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi where proxyResp smp = let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp - in case compatibleVRange thServerVRange proxiedSMPRelayVRange of + in case compatibleVRange thServerVRange proxiedSMPRelayVRange of -- Cap the destination relay version range to prevent client version fingerprinting. -- See comment for proxiedSMPRelayVersion. Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of @@ -718,54 +710,66 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi atomically (lookupSMPServerClient a sessId) >>= \case Just (own, smp) -> do inc own pRequests - if - | v >= sendingProxySMPVersion -> - liftIO (runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case - Right r -> PRES r <$ inc own pSuccesses - Left e -> case e of - PCEProtocolError {} -> ERR err <$ inc own pSuccesses - _ -> ERR err <$ inc own pErrorsOther - where - err = smpProxyError e - | otherwise -> ERR (transportErr TEVersion) <$ inc own pErrorsCompat + if v >= sendingProxySMPVersion + then forkProxiedCmd $ do + liftIO (runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case + Right r -> PRES r <$ inc own pSuccesses + Left e -> ERR (smpProxyError e) <$ case e of + PCEProtocolError {} -> inc own pSuccesses + _ -> inc own pErrorsOther + else Just (ERR $ transportErr TEVersion) <$ inc own pErrorsCompat where THandleParams {thVersion = v} = thParams smp - Nothing -> inc False pRequests >> inc False pErrorsConnect $> ERR (PROXY NO_SESSION) + Nothing -> inc False pRequests >> inc False pErrorsConnect $> Just (ERR $ PROXY NO_SESSION) + where + forkProxiedCmd :: M BrokerMsg -> M (Maybe BrokerMsg) + forkProxiedCmd cmdAction = do + bracket_ wait signal . forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " proxy") $ do + -- commands MUST be processed under a reasonable timeout or the client would halt + cmdAction >>= \t -> reply [(corrId, sessId, t)] + pure Nothing + where + wait = do + ServerConfig {serverClientConcurrency} <- asks config + atomically $ do + used <- readTVar procThreads + when (used >= serverClientConcurrency) retry + writeTVar procThreads $! used + 1 + signal = atomically $ modifyTVar' procThreads (\t -> t - 1) transportErr :: TransportError -> ErrorType transportErr = PROXY . BROKER . TRANSPORT mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> TVar Int) -> m () mkIncProxyStats ps psOwn = \own sel -> do atomically $ modifyTVar' (sel ps) (+ 1) when own $ atomically $ modifyTVar' (sel psOwn) (+ 1) - processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg)) - processCommand (qr_, (corrId, queueId, cmd)) = do - st <- asks queueStore - case cmd of - Cmd SProxiedClient command -> pure $ Left (corrId, queueId, command) - Cmd SSender command -> Right <$> case command of - SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody - PING -> pure (corrId, "", PONG) - RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock - Cmd SNotifier NSUB -> Right <$> subscribeNotifications - Cmd SRecipient command -> - Right <$> case command of - NEW rKey dhKey auth subMode -> - ifM - allowNew - (createQueue st rKey dhKey subMode) - (pure (corrId, queueId, ERR AUTH)) - where - allowNew = do - ServerConfig {allowNewQueues, newQueueBasicAuth} <- asks config - pure $ allowNewQueues && maybe True ((== auth) . Just) newQueueBasicAuth - SUB -> withQueue (`subscribeQueue` queueId) - GET -> withQueue getMessage - ACK msgId -> withQueue (`acknowledgeMsg` msgId) - KEY sKey -> secureQueue_ st sKey - NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey - NDEL -> deleteQueueNotifier_ st - OFF -> suspendQueue_ st - DEL -> delQueueAndMsgs st + processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Maybe (Transmission BrokerMsg)) + processCommand (qr_, (corrId, queueId, cmd)) = case cmd of + Cmd SProxiedClient command -> processProxiedCmd (corrId, queueId, command) + Cmd SSender command -> Just <$> case command of + SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody + PING -> pure (corrId, "", PONG) + RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock + Cmd SNotifier NSUB -> Just <$> subscribeNotifications + Cmd SRecipient command -> do + st <- asks queueStore + Just <$> case command of + NEW rKey dhKey auth subMode -> + ifM + allowNew + (createQueue st rKey dhKey subMode) + (pure (corrId, queueId, ERR AUTH)) + where + allowNew = do + ServerConfig {allowNewQueues, newQueueBasicAuth} <- asks config + pure $ allowNewQueues && maybe True ((== auth) . Just) newQueueBasicAuth + SUB -> withQueue (`subscribeQueue` queueId) + GET -> withQueue getMessage + ACK msgId -> withQueue (`acknowledgeMsg` msgId) + KEY sKey -> secureQueue_ st sKey + NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey + NDEL -> deleteQueueNotifier_ st + OFF -> suspendQueue_ st + DEL -> delQueueAndMsgs st where createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> M (Transmission BrokerMsg) createQueue st recipientKey dhKey subMode = time "NEW" $ do @@ -1036,7 +1040,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Right t''@(_, (corrId', entId', cmd')) -> case cmd' of Cmd SSender SEND {} -> -- Left will not be returned by processCommand, as only SEND command is allowed - fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + fromMaybe (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') _ -> pure (corrId', entId', ERR $ CMD PROHIBITED) -- encode response diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 77adb94f4..f602c890b 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -104,7 +104,7 @@ defaultInactiveClientExpiration = } defaultProxyClientConcurrency :: Int -defaultProxyClientConcurrency = 16 +defaultProxyClientConcurrency = 32 data Env = Env { config :: ServerConfig, diff --git a/tests/AgentTests/EqInstances.hs b/tests/AgentTests/EqInstances.hs index aaaa2de51..a810247fe 100644 --- a/tests/AgentTests/EqInstances.hs +++ b/tests/AgentTests/EqInstances.hs @@ -6,6 +6,7 @@ module AgentTests.EqInstances where import Data.Type.Equality import Simplex.Messaging.Agent.Store +import Simplex.Messaging.Client (ProxiedRelay (..)) instance Eq SomeConn where SomeConn d c == SomeConn d' c' = case testEquality d d' of @@ -23,3 +24,7 @@ deriving instance Eq (StoredSndQueue q) deriving instance Eq (DBQueueId q) deriving instance Eq ClientNtfCreds + +deriving instance Show ProxiedRelay + +deriving instance Eq ProxiedRelay diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 748eb34e7..0c5792d7a 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -12,6 +12,7 @@ module SMPProxyTests where +import AgentTests.EqInstances () import AgentTests.FunctionalAPITests import Control.Logger.Simple import Control.Monad (forM, forM_, forever) @@ -150,10 +151,13 @@ deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv -- get proxy session + sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") sess <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") + sess0 `shouldBe` sess -- send via proxy to unsecured queue forM_ unsecuredMsgs $ \msg -> do runExceptT' (proxySMPMessage pc sess Nothing sndId noMsgFlags msg) `shouldReturn` Right () + runExceptT' (proxySMPMessage pc sess {prSessionId = "bad session"} Nothing sndId noMsgFlags msg) `shouldReturn` Left (ProxyProtocolError $ SMP.PROXY SMP.NO_SESSION) -- receive 1 (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody})))]) <- atomically $ readTBQueue msgQ dec msgId encBody `shouldBe` Right msg From 199f85ec62bcbadd11dc05bb97cce6556dafb384 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 28 May 2024 11:56:57 +0100 Subject: [PATCH 070/125] agent: send MWARN on QUOTA errors (#1176) * agent: send MWARN on QUOTA errors * fix tests --- src/Simplex/Messaging/Agent.hs | 6 +++++- tests/AgentTests.hs | 2 ++ tests/AgentTests/FunctionalAPITests.hs | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 6f0f52589..76d7d333d 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1344,7 +1344,11 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE _ -> do expireTs <- addUTCTime (-quotaExceededTimeout) <$> liftIO getCurrentTime - if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RISlow + if internalTs < expireTs + then notifyDelMsgs msgId e expireTs + else do + notify $ MWARN (unId msgId) e + retrySndMsg RISlow SMP _ SMP.AUTH -> case msgType of AM_CONN_INFO -> connError msgId NOT_AVAILABLE AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 186c91e13..b200c3933 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -523,6 +523,7 @@ testMsgDeliveryQuotaExceeded _ alice bob = do (_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND F :" <> msg) alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False (_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND F :over quota") + alice <#= \case ("", "bob", MWARN _ (SMP _ QUOTA)) -> True; _ -> False alice #: ("1", "bob2", "SEND F :hello") #> ("1", "bob2", MID 4) -- if delivery is blocked it won't go further @@ -537,6 +538,7 @@ testResumeDeliveryQuotaExceeded _ alice bob = do (_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND F :" <> msg) alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False ("5", "bob", Right (MID 8)) <- alice #: ("5", "bob", "SEND F :over quota") + alice <#= \case ("", "bob", MWARN 8 (SMP _ QUOTA)) -> True; _ -> False alice #:# "the last message not sent yet" bob <#= \case ("", "alice", Msg "message 1") -> True; _ -> False bob #: ("1", "alice", "ACK 4") #> ("1", "alice", OK) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 250e95474..2b21ff3f7 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -148,6 +148,7 @@ pGet c = do CONNECT {} -> pGet c DISCONNECT {} -> pGet c ERR (BROKER _ NETWORK) -> pGet c + MWARN {} -> pGet c _ -> pure t pattern CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> ACommand 'Agent e From e55ec07fe215e6c1255e95c4113c88daba4f038e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 28 May 2024 15:32:41 +0100 Subject: [PATCH 071/125] server: log stats for QUOTA and other errors (#1177) * server: log stats for QUOTA errors * fix test * more stats * remove duplicate column --- src/Simplex/Messaging/Server.hs | 101 +++++++++++++++++--------- src/Simplex/Messaging/Server/Stats.hs | 66 +++++++++++++++-- tests/ServerTests.hs | 6 +- 3 files changed, 128 insertions(+), 45 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index adf0b5df7..0d209229c 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -228,7 +228,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath liftIO $ threadDelay' $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0) - ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv} <- asks serverStats + ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, qSub, qSubAuth, qSubDuplicate, qSubProhibited, msgSent, msgSentAuth, msgSentQuota, msgSentLarge, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv} <- asks serverStats let interval = 1000000 * logInterval forever $ do withFile statsFilePath AppendMode $ \h -> liftIO $ do @@ -240,7 +240,14 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do qDeletedAll' <- atomically $ swapTVar qDeletedAll 0 qDeletedNew' <- atomically $ swapTVar qDeletedNew 0 qDeletedSecured' <- atomically $ swapTVar qDeletedSecured 0 + qSub' <- atomically $ swapTVar qSub 0 + qSubAuth' <- atomically $ swapTVar qSubAuth 0 + qSubDuplicate' <- atomically $ swapTVar qSubDuplicate 0 + qSubProhibited' <- atomically $ swapTVar qSubProhibited 0 msgSent' <- atomically $ swapTVar msgSent 0 + msgSentAuth' <- atomically $ swapTVar msgSentAuth 0 + msgSentQuota' <- atomically $ swapTVar msgSentQuota 0 + msgSentLarge' <- atomically $ swapTVar msgSentLarge 0 msgRecv' <- atomically $ swapTVar msgRecv 0 msgExpired' <- atomically $ swapTVar msgExpired 0 ps <- atomically $ periodStatCounts activeQueues ts @@ -281,7 +288,15 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do <> showProxyStats pRelaysOwn' <> showProxyStats pMsgFwds' <> showProxyStats pMsgFwdsOwn' - <> [show pMsgFwdsRecv'] + <> [ show pMsgFwdsRecv', + show qSub', + show qSubAuth', + show qSubDuplicate', + show qSubProhibited', + show msgSentAuth', + show msgSentQuota', + show msgSentLarge' + ] ) liftIO $ threadDelay' interval where @@ -504,19 +519,25 @@ receive h@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiv forever $ do ts <- L.toList <$> liftIO (tGet h) atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime - (errs, cmds) <- partitionEithers <$> mapM cmdAction ts + stats <- asks serverStats + (errs, cmds) <- partitionEithers <$> mapM (cmdAction stats) ts write sndQ errs write rcvQ cmds where - cmdAction :: SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) - cmdAction (tAuth, authorized, (corrId, entId, cmdOrError)) = + cmdAction :: ServerStats -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) + cmdAction stats (tAuth, authorized, (corrId, entId, cmdOrError)) = case cmdOrError of Left e -> pure $ Left (corrId, entId, ERR e) - Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized entId cmd + Right cmd -> verified =<< verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized entId cmd where verified = \case - VRVerified qr -> Right (qr, (corrId, entId, cmd)) - VRFailed -> Left (corrId, entId, ERR AUTH) + VRVerified qr -> pure $ Right (qr, (corrId, entId, cmd)) + VRFailed -> do + case cmd of + Cmd _ SEND {} -> atomically $ modifyTVar' (msgSentAuth stats) (+ 1) + Cmd _ SUB -> atomically $ modifyTVar' (qSubAuth stats) (+ 1) + _ -> pure () + pure $ Left (corrId, entId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty send :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO () @@ -856,15 +877,19 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi subscribeQueue :: QueueRec -> RecipientId -> M (Transmission BrokerMsg) subscribeQueue qr rId = do + stats <- asks serverStats atomically (TM.lookup rId subscriptions) >>= \case - Nothing -> + Nothing -> do + atomically $ modifyTVar' (qSub stats) (+ 1) newSub >>= deliver Just sub -> readTVarIO sub >>= \case - Sub {subThread = ProhibitSub} -> + Sub {subThread = ProhibitSub} -> do -- cannot use SUB in the same connection where GET was used + atomically $ modifyTVar' (qSubProhibited stats) (+ 1) pure (corrId, rId, ERR $ CMD PROHIBITED) - s -> + s -> do + atomically $ modifyTVar' (qSubDuplicate stats) (+ 1) atomically (tryTakeTMVar $ delivered s) >> deliver sub where newSub :: M (TVar Sub) @@ -958,29 +983,37 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi sendMessage :: QueueRec -> MsgFlags -> MsgBody -> M (Transmission BrokerMsg) sendMessage qr msgFlags msgBody - | B.length msgBody > maxMessageLength thVersion = pure $ err LARGE_MSG - | otherwise = case status qr of - QueueOff -> return $ err AUTH - QueueActive -> - case C.maxLenBS msgBody of - Left _ -> pure $ err LARGE_MSG - Right body -> do - msg_ <- time "SEND" $ do - q <- getStoreMsgQueue "SEND" $ recipientId qr - expireMessages q - atomically . writeMsg q =<< mkMessage body - case msg_ of - Nothing -> pure $ err QUOTA - Just msg -> time "SEND ok" $ do - stats <- asks serverStats - when (notification msgFlags) $ do - atomically . trySendNotification msg =<< asks random - atomically $ modifyTVar' (msgSentNtf stats) (+ 1) - atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr) - atomically $ modifyTVar' (msgSent stats) (+ 1) - atomically $ modifyTVar' (msgCount stats) (+ 1) - atomically $ updatePeriodStats (activeQueues stats) (recipientId qr) - pure ok + | B.length msgBody > maxMessageLength thVersion = do + stats <- asks serverStats + atomically $ modifyTVar' (msgSentLarge stats) (+ 1) + pure $ err LARGE_MSG + | otherwise = do + stats <- asks serverStats + case status qr of + QueueOff -> do + atomically $ modifyTVar' (msgSentAuth stats) (+ 1) + pure $ err AUTH + QueueActive -> + case C.maxLenBS msgBody of + Left _ -> pure $ err LARGE_MSG + Right body -> do + msg_ <- time "SEND" $ do + q <- getStoreMsgQueue "SEND" $ recipientId qr + expireMessages q + atomically . writeMsg q =<< mkMessage body + case msg_ of + Nothing -> do + atomically $ modifyTVar' (msgSentQuota stats) (+ 1) + pure $ err QUOTA + Just msg -> time "SEND ok" $ do + when (notification msgFlags) $ do + atomically . trySendNotification msg =<< asks random + atomically $ modifyTVar' (msgSentNtf stats) (+ 1) + atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr) + atomically $ modifyTVar' (msgSent stats) (+ 1) + atomically $ modifyTVar' (msgCount stats) (+ 1) + atomically $ updatePeriodStats (activeQueues stats) (recipientId qr) + pure ok where THandleParams {thVersion} = thParams' mkMessage :: C.MaxLenBS MaxMessageLen -> M Message diff --git a/src/Simplex/Messaging/Server/Stats.hs b/src/Simplex/Messaging/Server/Stats.hs index d8935b44b..880791c3d 100644 --- a/src/Simplex/Messaging/Server/Stats.hs +++ b/src/Simplex/Messaging/Server/Stats.hs @@ -26,7 +26,14 @@ data ServerStats = ServerStats qDeletedAll :: TVar Int, qDeletedNew :: TVar Int, qDeletedSecured :: TVar Int, + qSub :: TVar Int, + qSubAuth :: TVar Int, + qSubDuplicate :: TVar Int, + qSubProhibited :: TVar Int, msgSent :: TVar Int, + msgSentAuth :: TVar Int, + msgSentQuota :: TVar Int, + msgSentLarge :: TVar Int, msgRecv :: TVar Int, msgExpired :: TVar Int, activeQueues :: PeriodStats RecipientId, @@ -49,7 +56,14 @@ data ServerStatsData = ServerStatsData _qDeletedAll :: Int, _qDeletedNew :: Int, _qDeletedSecured :: Int, + _qSub :: Int, + _qSubAuth :: Int, + _qSubDuplicate :: Int, + _qSubProhibited :: Int, _msgSent :: Int, + _msgSentAuth :: Int, + _msgSentQuota :: Int, + _msgSentLarge :: Int, _msgRecv :: Int, _msgExpired :: Int, _activeQueues :: PeriodStatsData RecipientId, @@ -74,7 +88,14 @@ newServerStats ts = do qDeletedAll <- newTVar 0 qDeletedNew <- newTVar 0 qDeletedSecured <- newTVar 0 + qSub <- newTVar 0 + qSubAuth <- newTVar 0 + qSubDuplicate <- newTVar 0 + qSubProhibited <- newTVar 0 msgSent <- newTVar 0 + msgSentAuth <- newTVar 0 + msgSentQuota <- newTVar 0 + msgSentLarge <- newTVar 0 msgRecv <- newTVar 0 msgExpired <- newTVar 0 activeQueues <- newPeriodStats @@ -88,7 +109,7 @@ newServerStats ts = do pMsgFwdsRecv <- newTVar 0 qCount <- newTVar 0 msgCount <- newTVar 0 - pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv, qCount, msgCount} + pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, qSub, qSubAuth, qSubDuplicate, qSubProhibited, msgSent, msgSentAuth, msgSentQuota, msgSentLarge, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv, qCount, msgCount} getServerStatsData :: ServerStats -> STM ServerStatsData getServerStatsData s = do @@ -98,7 +119,14 @@ getServerStatsData s = do _qDeletedAll <- readTVar $ qDeletedAll s _qDeletedNew <- readTVar $ qDeletedNew s _qDeletedSecured <- readTVar $ qDeletedSecured s + _qSub <- readTVar $ qSub s + _qSubAuth <- readTVar $ qSubAuth s + _qSubDuplicate <- readTVar $ qSubDuplicate s + _qSubProhibited <- readTVar $ qSubProhibited s _msgSent <- readTVar $ msgSent s + _msgSentAuth <- readTVar $ msgSentAuth s + _msgSentQuota <- readTVar $ msgSentQuota s + _msgSentLarge <- readTVar $ msgSentLarge s _msgRecv <- readTVar $ msgRecv s _msgExpired <- readTVar $ msgExpired s _activeQueues <- getPeriodStatsData $ activeQueues s @@ -112,7 +140,7 @@ getServerStatsData s = do _pMsgFwdsRecv <- readTVar $ pMsgFwdsRecv s _qCount <- readTVar $ qCount s _msgCount <- readTVar $ msgCount s - pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount} + pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _qSub, _qSubAuth, _qSubDuplicate, _qSubProhibited, _msgSent, _msgSentAuth, _msgSentQuota, _msgSentLarge, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount} setServerStats :: ServerStats -> ServerStatsData -> STM () setServerStats s d = do @@ -122,7 +150,14 @@ setServerStats s d = do writeTVar (qDeletedAll s) $! _qDeletedAll d writeTVar (qDeletedNew s) $! _qDeletedNew d writeTVar (qDeletedSecured s) $! _qDeletedSecured d + writeTVar (qSub s) $! _qSub d + writeTVar (qSubAuth s) $! _qSubAuth d + writeTVar (qSubDuplicate s) $! _qSubDuplicate d + writeTVar (qSubProhibited s) $! _qSubProhibited d writeTVar (msgSent s) $! _msgSent d + writeTVar (msgSentAuth s) $! _msgSentAuth d + writeTVar (msgSentQuota s) $! _msgSentQuota d + writeTVar (msgSentLarge s) $! _msgSentLarge d writeTVar (msgRecv s) $! _msgRecv d writeTVar (msgExpired s) $! _msgExpired d setPeriodStats (activeQueues s) (_activeQueues d) @@ -147,7 +182,14 @@ instance StrEncoding ServerStatsData where "qDeletedNew=" <> strEncode (_qDeletedNew d), "qDeletedSecured=" <> strEncode (_qDeletedSecured d), "qCount=" <> strEncode (_qCount d), + "qSub=" <> strEncode (_qSub d), + "qSubAuth=" <> strEncode (_qSubAuth d), + "qSubDuplicate=" <> strEncode (_qSubDuplicate d), + "qSubProhibited=" <> strEncode (_qSubProhibited d), "msgSent=" <> strEncode (_msgSent d), + "msgSentAuth=" <> strEncode (_msgSentAuth d), + "msgSentQuota=" <> strEncode (_msgSentQuota d), + "msgSentLarge=" <> strEncode (_msgSentLarge d), "msgRecv=" <> strEncode (_msgRecv d), "msgExpired=" <> strEncode (_msgExpired d), "msgSentNtf=" <> strEncode (_msgSentNtf d), @@ -173,12 +215,19 @@ instance StrEncoding ServerStatsData where (_qDeletedAll, _qDeletedNew, _qDeletedSecured) <- (,0,0) <$> ("qDeleted=" *> strP <* A.endOfLine) <|> ((,,) <$> ("qDeletedAll=" *> strP <* A.endOfLine) <*> ("qDeletedNew=" *> strP <* A.endOfLine) <*> ("qDeletedSecured=" *> strP <* A.endOfLine)) - _qCount <- "qCount=" *> strP <* A.endOfLine <|> pure 0 + _qCount <- opt "qCount=" + _qSub <- opt "qSub=" + _qSubAuth <- opt "qSubAuth=" + _qSubDuplicate <- opt "qSubDuplicate=" + _qSubProhibited <- opt "qSubProhibited=" _msgSent <- "msgSent=" *> strP <* A.endOfLine + _msgSentAuth <- opt "msgSentAuth=" + _msgSentQuota <- opt "msgSentQuota=" + _msgSentLarge <- opt "msgSentLarge=" _msgRecv <- "msgRecv=" *> strP <* A.endOfLine - _msgExpired <- "msgExpired=" *> strP <* A.endOfLine <|> pure 0 - _msgSentNtf <- "msgSentNtf=" *> strP <* A.endOfLine <|> pure 0 - _msgRecvNtf <- "msgRecvNtf=" *> strP <* A.endOfLine <|> pure 0 + _msgExpired <- opt "msgExpired=" + _msgSentNtf <- opt "msgSentNtf=" + _msgRecvNtf <- opt "msgRecvNtf=" _activeQueues <- optional ("activeQueues:" <* A.endOfLine) >>= \case Just _ -> strP <* optional A.endOfLine @@ -195,9 +244,10 @@ instance StrEncoding ServerStatsData where _pRelaysOwn <- proxyStatsP "pRelaysOwn:" _pMsgFwds <- proxyStatsP "pMsgFwds:" _pMsgFwdsOwn <- proxyStatsP "pMsgFwdsOwn:" - _pMsgFwdsRecv <- "pMsgFwdsRecv=" *> strP <* A.endOfLine <|> pure 0 - pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount = 0} + _pMsgFwdsRecv <- opt "pMsgFwdsRecv=" + pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _qSub, _qSubAuth, _qSubDuplicate, _qSubProhibited, _msgSent, _msgSentAuth, _msgSentQuota, _msgSentLarge, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount = 0} where + opt s = A.string s *> strP <* A.endOfLine <|> pure 0 proxyStatsP key = optional (A.string key >> A.endOfLine) >>= \case Just _ -> strP <* optional A.endOfLine diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index b0c90ca96..e2ca278d9 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -608,7 +608,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 2 logSize testStoreMsgsFile `shouldReturn` 5 - logSize testServerStatsBackupFile `shouldReturn` 45 + logSize testServerStatsBackupFile `shouldReturn` 52 Right stats1 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats1 [rId] 5 1 @@ -626,7 +626,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 -- the last message is not removed because it was not ACK'd logSize testStoreMsgsFile `shouldReturn` 3 - logSize testServerStatsBackupFile `shouldReturn` 45 + logSize testServerStatsBackupFile `shouldReturn` 52 Right stats2 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats2 [rId] 5 3 @@ -645,7 +645,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 logSize testStoreMsgsFile `shouldReturn` 0 - logSize testServerStatsBackupFile `shouldReturn` 45 + logSize testServerStatsBackupFile `shouldReturn` 52 Right stats3 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats3 [rId] 5 5 From ee052a454ee943cb54a805247d4df13e102ddb67 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 28 May 2024 15:33:55 +0100 Subject: [PATCH 072/125] 5.8.0.6 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 61f144eb1..7f548c9a4 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.5 +version: 5.8.0.6 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 55d8cd637..65a2c24be 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.5 +version: 5.8.0.6 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 63f5e76f9cb33742d3c0c812af0e342dffafca0c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 29 May 2024 08:06:01 +0100 Subject: [PATCH 073/125] agent: treat absent proxy session as a temporary error to retry sending (#1178) * agent: treat absent proxy session as a temporary error to retry sending * enable all tests --- src/Simplex/Messaging/Agent/Client.hs | 22 ++++++++- tests/SMPClient.hs | 3 ++ tests/SMPProxyTests.hs | 65 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 37f659c9f..a99d957e3 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1039,17 +1039,34 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do SPFAllowProtected -> ipAddressProtected cfg destSrv SPFProhibit -> False unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) - sendViaProxy destSess = do + sendViaProxy destSess@(_, _, qId) = do r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case Right () -> pure . Just $ protocolClientServer' smp - Left proxyErr -> + Left proxyErr -> do + case proxyErr of + (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> atomically deleteRelaySession + _ -> pure () throwE PROXY { proxyServer = protocolClientServer smp, relayServer = B.unpack $ strEncode destSrv, proxyErr } + where + -- checks that the current proxied relay session is the same one that was used to send the message and removes it + deleteRelaySession = + ( TM.lookup destSess (smpProxiedRelays c) + $>>= \(ProtoServerWithAuth srv _) -> tryReadSessVar (userId, srv, qId) (smpClients c) + ) + >>= \case + Just (Right (SMPConnectedClient smp' prs)) | sameClient smp' -> + tryReadSessVar destSrv prs >>= \case + Just (Right proxySess') | sameProxiedRelay proxySess' -> TM.delete destSrv prs + _ -> pure () + _ -> pure () + sameClient smp' = sessionId (thParams smp) == sessionId (thParams smp') + sameProxiedRelay proxySess' = prSessionId proxySess == prSessionId proxySess' case r of Right r' -> pure r' Left e @@ -1288,6 +1305,7 @@ temporaryAgentError = \case BROKER _ e -> tempBrokerError e SMP _ (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> True INACTIVE -> True _ -> False where diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 1828ffae6..f8c0e22c1 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -59,6 +59,9 @@ testStoreLogFile2 = "tests/tmp/smp-server-store.log.2" testStoreMsgsFile :: FilePath testStoreMsgsFile = "tests/tmp/smp-server-messages.log" +testStoreMsgsFile2 :: FilePath +testStoreMsgsFile2 = "tests/tmp/smp-server-messages.log.2" + testServerStatsBackupFile :: FilePath testServerStatsBackupFile = "tests/tmp/smp-server-stats.log" diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 0c5792d7a..036c8b203 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -14,6 +14,7 @@ module SMPProxyTests where import AgentTests.EqInstances () import AgentTests.FunctionalAPITests +import Control.Concurrent (ThreadId) import Control.Logger.Simple import Control.Monad (forM, forM_, forever) import Control.Monad.Trans.Except (ExceptT, runExceptT) @@ -113,6 +114,8 @@ smpProxyTests = do agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ agentViaProxyVersionError + it "retries sending when destination or proxy relay is offline" $ + agentViaProxyRetryOffline describe "stress test 1k" $ do let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) it "2 agents, 250 messages" . oneServer $ deliver 2 250 @@ -290,6 +293,68 @@ agentViaProxyVersionError = where servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs} +agentViaProxyRetryOffline :: IO () +agentViaProxyRetryOffline = do + let srv1 = SMPServer testHost testPort testKeyHash + srv2 = SMPServer testHost testPort2 testKeyHash + msg1 = "hello 1" + msg2 = "hello 2" + aProxySrv = Just srv1 + bProxySrv = Just srv2 + withAgent 1 aCfg (servers srv1) testDB $ \alice -> + withAgent 2 aCfg (servers srv2) testDB2 $ \bob -> do + let pqEnc = CR.PQEncOn + withServer $ \_ -> do + (aliceId, bobId) <- withServer2 $ \_ -> runRight $ do + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice + liftIO $ pqSup' `shouldBe` PQSupportOn + allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + 1 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + get alice ##> ("", bobId, A.SENT (baseId + 1) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 1) Nothing + 2 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + get bob ##> ("", aliceId, A.SENT (baseId + 2) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 2) Nothing + pure (aliceId, bobId) + runRight_ $ do + -- destination relay down + 3 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + bob `down` aliceId + withServer2 $ \_ -> runRight_ $ do + bob `up` aliceId + get alice ##> ("", bobId, A.SENT (baseId + 3) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 3) Nothing + runRight_ $ do + -- proxy relay down + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + bob `down` aliceId + withServer2 $ \_ -> runRight_ $ do + bob `up` aliceId + get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 4) Nothing + where + withServer :: (ThreadId -> IO a) -> IO a + withServer = withServer_ testStoreLogFile testStoreMsgsFile testPort + withServer2 :: (ThreadId -> IO a) -> IO a + withServer2 = withServer_ testStoreLogFile2 testStoreMsgsFile2 testPort2 + withServer_ storeLog storeMsgs port = + withSmpServerConfigOn (transport @TLS) proxyCfg {storeLogFile = Just storeLog, storeMsgsFile = Just storeMsgs} port + a `up` cId = nGet a =##> \case ("", "", UP _ [c]) -> c == cId; _ -> False + a `down` cId = nGet a =##> \case ("", "", DOWN _ [c]) -> c == cId; _ -> False + aCfg = agentProxyCfg {messageRetryInterval = fastMessageRetryInterval} + baseId = 3 + msgId = subtract baseId . fst + servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]} + testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do From 0f663bd569f5d282fc13cd969391f30d873d73a0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 29 May 2024 08:09:27 +0100 Subject: [PATCH 074/125] 5.8.0.7 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 7f548c9a4..02095d95f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.6 +version: 5.8.0.7 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 65a2c24be..890ecf1e8 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.6 +version: 5.8.0.7 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 5c2c88315a0d702b53094b7678cc68d18a8095a9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 29 May 2024 11:30:42 +0100 Subject: [PATCH 075/125] SMP server information (#1072) * SMP server information * fix tests * country codes * smp-server: serve contact and link pages from static files (#1084) * smp-server: serve contact and link pages from static files * generate index * use params from ini * render using ServerInformation * tweak templates * update * fix some html * smp-server: fix layout (#1097) * smp-server: fix layout * port fixes to link page --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> * update server information page --------- Co-authored-by: Evgeny Poberezkin Co-authored-by: M. Sarmad Qadeer * update server info * web: improve server info page design (#1166) * web: improve server info page design * web: fix font errors & some tags * web: improve contact & invitation page layout and header * update * remove unused files/css * cleanup * fix link page * remove unused font links --------- Co-authored-by: Evgeny Poberezkin * show contact address as is --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Co-authored-by: M. Sarmad Qadeer --- apps/smp-server/Main.hs | 5 +- apps/smp-server/static/contact/index.html | 1 + apps/smp-server/static/index.html | 514 +++ apps/smp-server/static/invitation/index.html | 1 + apps/smp-server/static/link.html | 527 +++ apps/smp-server/static/media/GilroyBold.woff2 | Bin 0 -> 30884 bytes .../smp-server/static/media/GilroyLight.woff2 | Bin 0 -> 29968 bytes .../static/media/GilroyMedium.woff2 | Bin 0 -> 30648 bytes .../static/media/GilroyRegular.woff2 | Bin 0 -> 29136 bytes .../static/media/GilroyRegularItalic.woff2 | Bin 0 -> 31452 bytes apps/smp-server/static/media/apk_icon.png | Bin 0 -> 18130 bytes apps/smp-server/static/media/apple_store.svg | 26 + apps/smp-server/static/media/contact.js | 66 + .../static/media/contact_page_mobile.png | Bin 0 -> 295874 bytes apps/smp-server/static/media/f_droid.svg | 372 ++ apps/smp-server/static/media/favicon.ico | Bin 0 -> 1150 bytes apps/smp-server/static/media/google_play.svg | 39 + apps/smp-server/static/media/logo-dark.png | Bin 0 -> 7383 bytes apps/smp-server/static/media/logo-light.png | Bin 0 -> 8262 bytes .../static/media/logo-symbol-dark.svg | 10 + .../static/media/logo-symbol-light.svg | 15 + apps/smp-server/static/media/moon.svg | 3 + apps/smp-server/static/media/qrcode.js | 1 + apps/smp-server/static/media/script.js | 39 + apps/smp-server/static/media/style.css | 410 +++ apps/smp-server/static/media/sun.svg | 11 + .../static/media/swiper-bundle.min.css | 13 + .../static/media/swiper-bundle.min.js | 14 + apps/smp-server/static/media/tailwind.css | 3058 +++++++++++++++++ apps/smp-server/static/media/testflight.png | Bin 0 -> 18151 bytes apps/smp-server/web/Static.hs | 176 + apps/smp-server/web/Static/Embedded.hs | 15 + package.yaml | 10 +- rfcs/2024-03-20-server-metadata.md | 33 +- simplexmq.cabal | 35 + src/Simplex/FileTransfer/Server/Main.hs | 19 +- src/Simplex/Messaging/Agent/Protocol.hs | 59 +- .../Messaging/Notifications/Server/Main.hs | 12 +- src/Simplex/Messaging/Server/CLI.hs | 2 +- src/Simplex/Messaging/Server/Env/STM.hs | 30 +- src/Simplex/Messaging/Server/Information.hs | 76 + src/Simplex/Messaging/Server/Main.hs | 279 +- tests/CLITests.hs | 2 +- tests/SMPClient.hs | 3 +- 44 files changed, 5780 insertions(+), 96 deletions(-) create mode 120000 apps/smp-server/static/contact/index.html create mode 100644 apps/smp-server/static/index.html create mode 120000 apps/smp-server/static/invitation/index.html create mode 100644 apps/smp-server/static/link.html create mode 100644 apps/smp-server/static/media/GilroyBold.woff2 create mode 100644 apps/smp-server/static/media/GilroyLight.woff2 create mode 100644 apps/smp-server/static/media/GilroyMedium.woff2 create mode 100644 apps/smp-server/static/media/GilroyRegular.woff2 create mode 100644 apps/smp-server/static/media/GilroyRegularItalic.woff2 create mode 100644 apps/smp-server/static/media/apk_icon.png create mode 100644 apps/smp-server/static/media/apple_store.svg create mode 100644 apps/smp-server/static/media/contact.js create mode 100644 apps/smp-server/static/media/contact_page_mobile.png create mode 100644 apps/smp-server/static/media/f_droid.svg create mode 100644 apps/smp-server/static/media/favicon.ico create mode 100644 apps/smp-server/static/media/google_play.svg create mode 100644 apps/smp-server/static/media/logo-dark.png create mode 100644 apps/smp-server/static/media/logo-light.png create mode 100644 apps/smp-server/static/media/logo-symbol-dark.svg create mode 100644 apps/smp-server/static/media/logo-symbol-light.svg create mode 100644 apps/smp-server/static/media/moon.svg create mode 100644 apps/smp-server/static/media/qrcode.js create mode 100644 apps/smp-server/static/media/script.js create mode 100644 apps/smp-server/static/media/style.css create mode 100644 apps/smp-server/static/media/sun.svg create mode 100644 apps/smp-server/static/media/swiper-bundle.min.css create mode 100644 apps/smp-server/static/media/swiper-bundle.min.js create mode 100644 apps/smp-server/static/media/tailwind.css create mode 100644 apps/smp-server/static/media/testflight.png create mode 100644 apps/smp-server/web/Static.hs create mode 100644 apps/smp-server/web/Static/Embedded.hs create mode 100644 src/Simplex/Messaging/Server/Information.hs diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index d5cc5e732..d0de4b4f1 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -1,10 +1,9 @@ -{-# LANGUAGE LambdaCase #-} - module Main where import Control.Logger.Simple import Simplex.Messaging.Server.CLI (getEnvPath) import Simplex.Messaging.Server.Main +import qualified Static defaultCfgPath :: FilePath defaultCfgPath = "/etc/opt/simplex" @@ -20,4 +19,4 @@ main = do setLogLevel LogDebug cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath - withGlobalLogging logCfg $ smpServerCLI cfgPath logPath + withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath diff --git a/apps/smp-server/static/contact/index.html b/apps/smp-server/static/contact/index.html new file mode 120000 index 000000000..1140bcf31 --- /dev/null +++ b/apps/smp-server/static/contact/index.html @@ -0,0 +1 @@ +../link.html \ No newline at end of file diff --git a/apps/smp-server/static/index.html b/apps/smp-server/static/index.html new file mode 100644 index 000000000..e3889de2a --- /dev/null +++ b/apps/smp-server/static/index.html @@ -0,0 +1,514 @@ + + + + + + + SimpleX Chat - Server Information + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ +
+
+
+

Server + information

+ +
+
+

+ Public information +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Source code:${sourceCode}
Web site:${website}
Usage conditions:${usageConditions}
Amendments:${usageAmendments}
Operator:${operatorEntity} (${operatorCountry})
Administrator contacts: + +
Complaints and feedback: + +
Hosting provider:${hostingEntity} (${hostingCountry})
Server country:${serverCountry}
+
+ +
+

+ Configuration

+ + + + + + + + + + + + + + + + + + + + + +
Persistence:${persistence}
Message expiration:${messageExpiration}
Stats enabled:${statsEnabled}
New queues allowed:${newQueuesAllowed}
Basic auth enabled:${basicAuthEnabled}
+
+
+
+
+ +
+
+

+ Join SimpleX +

+

We invite you to join the conversation

+ +
+

Get SimpleX desktop app

+
+
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/apps/smp-server/static/invitation/index.html b/apps/smp-server/static/invitation/index.html new file mode 120000 index 000000000..1140bcf31 --- /dev/null +++ b/apps/smp-server/static/invitation/index.html @@ -0,0 +1 @@ +../link.html \ No newline at end of file diff --git a/apps/smp-server/static/link.html b/apps/smp-server/static/link.html new file mode 100644 index 000000000..cbab7324f --- /dev/null +++ b/apps/smp-server/static/link.html @@ -0,0 +1,527 @@ + + + + + + + SimpleX Chat - Invitation + + + + + + + + +
+
+
+ +
+
+ +
+ + + +
+
+
+

You received a + 1-time link to connect on SimpleX Chat

+

To make a + connection:

+
+

Install SimpleX app

+
+
+

1

+
+
+
+

Connect in app

Open + Simplex app +
+

2

+
+
+
+

Tap the‘connect’button in the app

+
+

3

+
+
+
+
+
+ + + +
+
+

+ Join SimpleX +

+

We invite you to join the conversation

+ +
+

Get SimpleX desktop app

+
+
+
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/apps/smp-server/static/media/GilroyBold.woff2 b/apps/smp-server/static/media/GilroyBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..687474f86e9c7195aa6911da93cee031222afad6 GIT binary patch literal 30884 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 AA^-pY literal 0 HcmV?d00001 diff --git a/apps/smp-server/static/media/GilroyLight.woff2 b/apps/smp-server/static/media/GilroyLight.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f4d4fc3e0c5d9de22e03386d2c2845f1019c8cea GIT binary patch literal 29968 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 literal 0 HcmV?d00001 diff --git a/apps/smp-server/static/media/GilroyMedium.woff2 b/apps/smp-server/static/media/GilroyMedium.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..895b6d4f1bdf009e723d7e12b85185417ee6abd0 GIT binary patch literal 30648 zcmZs>W3Vth4=ws^+qP}nwr$(CZQHhO+qP|c@AH0l?)i7pKdaM8GEI|CCtYrGqKtCN z%5wkyxwZd;{`a3O5&wtH|A+ox0u#_7gGq}OQ0a@E?F*{^FA5xh6_6MxfgBhVBv=p( zA4`033|1Zv3+$i&rAK&S;mZ({Vv^^$vWf~1DX?4JCLI6!McUr?Eu7!y)ZXty-QV|J zUcixO^^@Ynl~Ip#C9iD}cbm%FtS zbH}6_K$I@f%L;Z~hx|x)mFP@Bl#h!%>fLv#Jt_6 z*J#F!St&+>EVSG#TXQu4K4Z+>Xw9%siKuN{s@NBL?0Y>j%G=DWLw{D_GbIF8NuiOZck_b z5u#eRH{0Sn15iPNV-|19Bi#(&sXb&^;@7PA1MVn7h*(gPtAId9HZ-R@-kw5F{4F?> zY~*C@^z;g#N!T{oEI<~jHYIlf*WlAqU<$LOTX}5f-9xzrJF(ChKcxlab698uR=oa5 zPrH85_`&OS=Vfh|3e%zovgY9tm) zCU;Rx8z*Uwi{IjX?+5mM?z_m#*eM>4p+<^=zy-L6L46Blhsq_{O|)}GgR&ernqWuLbQ0?x(^5=L z4vBsdC>%G9OoZcuLJ6*flZ%Yz3K<2JApAY>tJ`z_&6P(HZ6rhp#kC0g!lNo9T znU|8+M}UpCay*ug8j>g$)C?X0+gN?#Kl}MGLva+2#k5#4xDi#U>3sW!6_oL$FID)=H8;v+HD7P4wxOs_!@<38fcvqQ;sHEv{k^5@J^8LI&-a_)77*NY zs|KrXpU03uz+?C0eB%Qb;)5^>x{gDQ6kscdY|dkI3aXvM4e3))2Y=KfvktnhLy;Xo zZwG_jL+|#(y@!SFp2roS2Ti2oJQ*lZhoIE6vJR-Oqj(+hW5*ylfXQCMQ?eAMF6VYG@?**#S+LHOl{AO4E&*`L zo}Dx96q7xt@-FeAx|-_2?#ss>y2~CyXXiXQD9{e2bfB^wqi*MRIq2aI7I`4f9fNdd zuRW;l4uyRn>m6fx=jAs$7{H*_7b4guSjAaH^J_Tk8~4 zxdhp+`FJYNotyNOz`ca@uGxLA4Qo$t1yV0AYVlH8xm35jG$ybPrEaluS*l$(_&D-| z$%Xyn1?CDc(B~cxf-Dch&?iubAgqUl4geZB*jKlwWCzoRt_5ZbMhNPucjEnSXIh)aPuuwzx`yMO8F1 zt;8{~@b0*{g2v*S4x6P9P^Ygrtpa3HXYM35oF{7_9!bxhPCjK1cIQ61_jb>E=K(W| zdh=rG&JP$0xEYU@=g@IBnK?bKgk;Br%nlN(Q6nddM0NbIqkbvBYaosq>pSpA;8Vz; zi2=SPl$3TqqtQ8C(@uena-8bdf|~B-{vF~HHR;pv+ zA}8OhlXH@wo0I{TpWbjrXD*||6hY98L5w&A&Tz$2ClP4d+_{SmkYk7-G#rp;3ET{gc3Q9QH)m_+PaGhV4{nZ0hd7+5i%0$l@FCm#4ONf3L1||g&tt&c9H$( z*W>^CtpMZ;=vE5}51>?^T9+^|u9p>Jh!mCRr*K!%WdwFp8L#s!qs9%osx7-$L{>^x zQdU$}TJ~&<9d-Kur^`ypUMxIqhclU~R63l(z;h+ZA=wG8y02Dgx z>5BlGgg(Ir>GS1cjRK-T1hv!=>&}ei8V{l{P z2m&N%0ceT_4{9l3Mc@GcXny1Xes~gmB!2jK#M2TDHjzkt_;`eHLU`~(U}HdVz(tW6 zXd)(2oUYaNh`7lK`hACJCd8O81z00mb6iQX^r{$Q1O^iY+0dYkt!M{{RTK7D;DJQ& z7UU%6pNjsS+I^s$D0~Su*ivCdR7KGe3q?{1V*70r?s4x7Q;9$lp6e8#%cFK)nYmh& zPId9fBmYnW)WA2h9e4K+NeQDUj&*J z?_PxixK$4~dYZd91FF#vs!=$nYi|d2Gy8RJ?EBzI11R#V6Q2%)6?WXy)9yusz+&O& z1bjD#72fIIqsoh$w&WDad}7H;X!0>~Ir2G5I?6n9J&HXlz6!3Yu1c?czuB5um9+An z%Zy8tOPEWWOSa3oOTEp~P1sFvn@~}ff}1Lw>YV(X8mdpv&(BZPPu9=bPja5Jv=@p` zZk#Ki&wx*;&$UtUB7@|r(S%Wo(TvfUQJ>MJQL)js(Z5sC)6`ShQ*)=l3^q5G zH%6^fLncN!`Z@X=f~s{|*YKVL*V{1?x{<2K&29p@2rZi_&2#ib-^nOhze%RuJmat8t>ztd5SA|!OSCUti*PK`FF9@&PpVA#OKf6DJzq+44ONAG>6}=Qo3KssF za#ba(a+l>#%W}+$R>iHV2Ft?C3RuOkDx~F%OG1~BPSu@aTP3$Duw@}niGZ3PNyzyo z)#}O3n|2hBe1)QM+n7VO5;GWw7tD1!g4G3by3>)U?rklOAJ-Sno63NZ7al9HvXWQ4 z^{O-f6|J^vjgGZ^p4N5VE7$AIdeXE0Qve7FpU)9YO%9B1U1elXqzz$oxhn}Cer8;< zpp+26%qhorFqQmDk|B_kKN0Alg6-7VD_Ra(+%2!OivwDPE1kE-jVE0ex`adG7HOl; zsPB4t#7)E)yNa_f7S~|6SSU&?m*K576*APvv()(fw3K`>&u+aw&z`(KcOH`|Pi}zF zQnPSJz-cHF;`4yR2{YG>RF+Xt$(a6|oON$sd#AqP;ocu3&u5KRqjrSM{>>V`Y|p8W z$LR$yeHXRKHb^3@4NaZzea0@e)MmO~NfVtke`T-csp)e)VAwKunPhN-!pRbYE%O=Awdm`q9 z7M;8-&#jN|)-AW%Y9L`cO27f2sa&t|r=eF|8t*8S_# z%wjoD3B6}$b3E?w4y~P0h&B!vS#Ni9o5?vzDH}aro}$$S7bz#O2IfA;GbD|f?MuJE zjLw{x52in8uDb`8?%lKUj-36d#}B?AhcCXDv`=JPk?CHC@$l9 zJO}ZFbSJ#`r=zQm0leDFDgFTDi69WlQQf9DOtv~6>RPeRh^99W+doB%#6%|~w?L5XvYc-iX zYMeUiCk-8brgZMkI-JU6a;_)^=G3>Ui@U2}W!S8QrSMP6fqw)#q4E7dJH-TZIr_7; zHH9P6vFGU?sI3PC+eJ&Tf)yQ#I{xiwR>Ygs8pDuEIH7>?8a+IZMB%6dmLv6cQdF>~ z%4l3^gQC=6?fGoHt z5_x?<+qiY*LCi6hqt7lsId%$*jf^%kl0z!7k7`-DM2-od&&}c8BIB-hC#JCrsh7rR z@?(pFNJP?=>*#<|6_K2?<3*fkgjgLShnylbsFwb!FF03CGkD}=nLHL@oU1iC&CZ}E za5ShvyC=Hb9$0~)QoRL4c9?GwM^rVSg7v_D?xE<7=K*{Y^nKFImFEmyzCm9o4?uWY z$ktpCw2X#TGlwYz`cne9im-+1Q+J>7EllVafWHR7sgaZO+eITxl6%N5JmRc?!JqW-lfs$ z-kr(m%JKV;AsL%{F17j;B0;0;mW>8QAdZLq=7_8X4t~;G*g8{TR_v&S^6vGYlTx$P zeJ%W>?icpA@T^~im(cuDeR%RQ!I z)cdao6iz(NU+9Uc7UI&%;nK`e)iRM8&K}$FX8_d!+wgayI;ajMCaqlu>~GKzF4uQ- zFoteHua=@&!fyL#Ug8L4GX?7_QPU;zW_^2pTm)`ID3;`bCCG_q$aM%b0L6w0Bsm^s zfK4VEZDSE6hubSAtm7I>-8SzcH1M2lei-{eY{}lh?!OC!9i@PtTCm`nri>7M5?^-C zR=p`;d0L8xWSu-YkRE)90Xv8uT&RHrQ~^Fj>0LpB24hRIr3$=dM{FL!Gf0e5F3}xjZ+>OXsRVZtO&{tt*~-?6vAyBe$)}*3M!({4YWEiF zF3^moR;6YW(DAsO!e7H(s*JCG1TiAwdnrfiZkMGU*|t6Ivn7k|O)o8Bp-xY9wDUI$CR}1CA*aVh%=UJ* zxV?S37#+x2a|?XUvEEd94A)<1+Tsl4BduE44~~^`2q1s~IgkJu$Qg{w7mHweg45nA z5pR{X%+SV)a*2JG@vDhnwyFAe8U|U209laX{($H=<}dm0p9U7}?H&|b3Iq`VltX@^ z`iP85xiSnj$f|MP#VEu*^b$%;AUQZ>A%-#38YG12JtBl@{TAe^&?*2^RY_QrhD_G0 z^)hAcLFp`6&49$ImW_;8b;kT~=Q^?8@1#t1@MMZADx}%^Su>*HUYJoHmaDP%MA={J z^390Em`Kd7A;B*%ICI(6X~3UWl!`RQtq2wZrbXZJvnwWqG%|T)py_7NM zwSX!3g|I0FBfb*>(X|RA94!7*h%_M-0aT+n14d&c^$I)q#z8zORS1PWbg3Y$Iv|IL zDwQMA)joTp?s$;(@;DS3^G0QYoN<{bzyWk4+AQH$K71qIECEZFV@%T|YYr|rWs;uPscZB0q53xXo! zr0cXJN5$Gnmkuhr&8D&#brmBpcX**Sht*oHwg;PYQ zDvv#Hb6(cIbU&J~l<`S@mBvc-mFiGcE~|K!Elty!hE}bz>Z(NL=y#NdzOh&0N8$(LSLA2r2j^$&SL-M3*X}p(XYW_E zO`{u(S3|Fyo~2zIyvDipwd=1}sjr@&rQf-$$I>^`*V9+ld)KGe$Jr;^hu!DhSKcSz zw}Yqmz+Q1ZnLW-u)IEYdh&`4)xIMZ(JiYb1S1sSTy|cL2vM0Gmxre(ayeGZKy+=Rr zb|4NQ8bByOGr+T;T>T~eEd4J1F#Wsz)cvk~J_4R(MrK}S9%i;?_GZpzR^(V}1Pzi# z3FEZ!l7uNDHPPH?ND-1qNxT%6N`2*=asq{>LR8_J7)`tumP_^Jjq0ElwwPN~ z9!gJv=hkzd1-Qb#VdJO^^d$~ct?9gU`|3!S;mZbB>x}i)hP1%`>y-sdk620e8P>V-;kmxj{+9?MdzW?XGLJGf?cZjhWd{4cral z(s3z~E3q%pK4aeIMgY`-CG~`>{hR7|Sq56yfjthuu_F-d7-mO^+96o>Sfu;D+yQ9w zLtO93di!CHemQ<$J`rt7qCXuu4by*zy|^n~?mUQ6?Yp;_wxx3sym}^eUWO5P8Qr(N zxH>!0dYA8>%5~M9ySJCNrFM2!Okaf+_x$PzD~*#DNE>;g%0Rm$22z6iqQ33|&|=>cQijsccw*I{BZr0P$bSzx~zic)EDOjKX&LlAaEf` zT?WJ2OtpA7i}|!)FNQrp9oZDQyqhj3NY zX@K1e2FVmdHDO&5d@rLR3Z>M~6=zrrfG?UbHX3ck&N){)i#(2@$)Y_wA3%c;+2vAgbXV*wU;7P?(lrBicJ4E8=QxLzZ-QA|d zMJn`dhoa4wqcw>~bemF`_P8+k_eB7%W-N@sXo+9q%npErH;WqgG zQxjaDbGP!{`0P39V#Q&mne$aBoWV`XOKp-lhZ@23*dkM_7T~ZpVi!>PK=oq`S53Gb zt=uRa#JjTbG^J&M-{Sqr*Z-HeZr zOm}r{8+`JG|KzqzBuOQcJ(pSM6~o78_z#pzBeV})I0)MPe+&0zf21|}q#A}Eq42m{ zcN9&{v_wxv+O{xrC_l;}w6TbmgZfq`*ax20+4ExH)1pNxAE(<2`mN6`zR*Z#dQ6DI zfEl~1zkp2&Ml?F&yD_drWa?9M^L@XU>dBfWK_dBXLS9s%j#f920A+7=v49||Y#(I~ zmyh>3MM}v)x$80uvuh$}lecJK>2)u4ncm%+6C6)F(OI()&tA7h0BO=*Omr;p(WfDW zpH&@7aGmdRT+$PqtT&Huu-3;jqLP=FHyfnC$p(`J3R+;p?4wCiW7gEItHTI@-CUfI z%#&3wpr-m_ICuTV*i~&?<|TH=G!ssasBRo$=Yt62mF4P+ojb|Ob(_t@>i46^aHJd@ z#lrMybjgE}8M0|@TC??8i3;qniQ1czzTzjYX5`doy|Rx3h9g-YB$ zGB{2(bMwadNkj;<7umB2XW$6f0nc|7iB z5pUk5foA=084N;8<%XY;3A!9R8^WwN@JSq1evlp_+M~EYb}ndHrxBQXusWA?wq=RB znflK~!3UT4imIeym!7-G@5^d;U3M;Nt?g2B3jFjgWZf9%DRHR*d#ny(>Q&dOpYe81 zAA<}RVp8kgkCRMl9gI$mnC>!cAxFAit5s3woHmnF6dgP&Nl$H5Wk{s-Wg;1fjlFu|BOxl zQpe4z8?^^hhJOiRbS{}hGK)6?eR>u$2w{|zhH>vCEMBi_NNf*LcX2ZeqstQYqIes}e;{Ax z8I{h8Sn>N2LW0$CM@*^`DLom~B1rttPy7vPp+uJp{D zH7p)|T5Lx`F7t9;`CEa`nFe{=)|B4z>10Gy1c+ruxIHTPdTjQ>iz~mBd07kd{qaOz*HNs_g&wyT9E<_l;n=gdZmhEzK-aw(9J*tU{?=|)D{@z|6Ri$_+v1XnyP3hv@-QO`0FP99F zm`%tVA!^{Zh0^B<$|}-Ua>W;~^YqLBokx~v)kGIl-4e%2h(fz5ktK9{iyVEuegQmZ zlBXoB&Z8e1Oy7u5@Ft@PTJ-Yt^^lJsOkt36Hd@meicrK(KG`0Ui+U+Q)^%q~VfSzA zCQqVAxgzB?=2_55J`j$&6}p_{(bBq^7@wL+J4SzcSEcj)4FR5MJx#6JZ9Sjdgq>EV zP^r>rAvxv88l7l+#ABAe`cT%T0?j$>+8cuKYPr{*{+= z=Lx0Ol*pV_C+uUQ|HX_`oAa1?8&+YEyndKv-p%ea-61=TP0_R7Jab&V9NA1ZfFhIq z-!`u+Xvi#jmGUG+kmKGOrqfC9lP9s>lLUs_ua(St6e#*BPQ%6RG71iEMAI%*86uLR zja?`blf5c$8{yUnS^i@61iYb~OmZ3MwFWHM{>or5i=O9a@#Nj@aB|JjU@1SvA5dxH z5mxKbKkdztY4{VmktN0VlgTL¥bNSh|gNdoVz7LdwFQrihxxYgV{ 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 literal 0 HcmV?d00001 diff --git a/apps/smp-server/static/media/GilroyRegular.woff2 b/apps/smp-server/static/media/GilroyRegular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..24465fb7dc8593f0924f14682d1e1ddfef48588c GIT binary patch literal 29136 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 literal 0 HcmV?d00001 diff --git a/apps/smp-server/static/media/apk_icon.png b/apps/smp-server/static/media/apk_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..60ff342d36a27c8fc41664391334a980c4ebfb89 GIT binary patch literal 18130 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 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/contact.js b/apps/smp-server/static/media/contact.js new file mode 100644 index 000000000..b1a99b74f --- /dev/null +++ b/apps/smp-server/static/media/contact.js @@ -0,0 +1,66 @@ +(function () { + + let complete = false + run() + window.onload = run + + async function run() { + const connURIel = document.getElementById("conn_req_uri_text"); + const mobileConnURIanchor = document.getElementById("mobile_conn_req_uri"); + const connQRCodes = document.getElementsByClassName("conn_req_uri_qrcode"); + console.log(connQRCodes); + if (complete || !connURIel || !mobileConnURIanchor || connQRCodes < 2) return + complete = true + let connURI = document.location.toString() + const parsedURI = new URL(connURI) + const path = parsedURI.pathname.split("/") + const len = path.length + const action = path[len - (path[len - 1] == "" ? 2 : 1)] + parsedURI.protocol = "https" + parsedURI.pathname = "/" + action + connURI = parsedURI.toString() + console.log("connection URI: ", connURI) + mobileConnURIanchor.href = "simplex:" + parsedURI.pathname + parsedURI.hash + connURIel.innerText = "/c " + connURI + for (const connQRCode of connQRCodes) { + try { + await QRCode.toCanvas(connQRCode, connURI, { + errorCorrectionLevel: "M", + color: {dark: "#062D56"} + }); + connQRCode.style.width = "320px"; + connQRCode.style.height = "320px"; + } catch (err) { + console.error(err); + } + } + + function contentCopyWithTooltip(parent) { + const content = parent.querySelector(".content"); + const tooltip = parent.querySelector(".tooltiptext"); + console.log(parent.querySelector(".content_copy"), 111) + console.log(parent) + const copyButton = parent.querySelector(".content_copy"); + copyButton.addEventListener("click", copyAddress) + copyButton.addEventListener("mouseout", resetTooltip) + + function copyAddress() { + navigator.clipboard.writeText(content.innerText || content.value); + tooltip.innerHTML = "Copied!"; + } + + function resetTooltip() { + tooltip.innerHTML = "Copy to clipboard"; + } + } + + function copyAddress() { + navigator.clipboard.writeText(connURI); + tooltipEl.innerHTML = "Copied!"; + } + + function resetTooltip() { + tooltipEl.innerHTML = "Copy to clipboard"; + } + } +})(); diff --git a/apps/smp-server/static/media/contact_page_mobile.png b/apps/smp-server/static/media/contact_page_mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..7d829965a8d29b518f4b25f50d6ed7c75fe92d6a GIT binary patch literal 295874 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%0AsdNppocv=evgT6SJ(a@6xWt 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

}%MZpNZp?kkhk6hNC4}TD%{! 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 + + + + + + + + + + + + GET IT ON + F-Droid + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/favicon.ico b/apps/smp-server/static/media/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c45f3acf04d103cbca57e3d1e6c12060068bb2dc GIT binary patch literal 1150 zcmb7@u}T9$5QgVauuUaeX*yp*tbG$-LPMUyU=gjflk+SBRtX`HPOE@msv~V`1!4SV zm${sKQDefN-JO~5-`TyKNXEC-5`GWmY+GbkM2-+4A~DV<0fUS=0*onEy&X>8rv zSfnWTJnRF~e|Y5D$D^O+dXp7MZFIj)#=zdf-33*>tYc4iw$9Tyu|n@*qZ+(Ew!`#8ma_ z`9Jw7Jxt*z@DurIZ#zeI7WdYfi+5SRIcLef=CNSDV7^>E|9WjYr>=uwSM|n* y*s}Rvro64qBaIVlywjmpMLw=Y-Va1RPojGJB3*8FJdUz?Z;#w@%v-H7sv=*^6aQZT literal 0 HcmV?d00001 diff --git a/apps/smp-server/static/media/google_play.svg b/apps/smp-server/static/media/google_play.svg new file mode 100644 index 000000000..6cda5f6d3 --- /dev/null +++ b/apps/smp-server/static/media/google_play.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/logo-dark.png b/apps/smp-server/static/media/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1886df819cd104659cc0d3a77c1afc6c83434bee GIT binary patch literal 7383 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 Z0p&QCf+W)r=pFV4JXO>{l*yY1{U7O^mC*nI literal 0 HcmV?d00001 diff --git a/apps/smp-server/static/media/logo-symbol-dark.svg b/apps/smp-server/static/media/logo-symbol-dark.svg new file mode 100644 index 000000000..fa598acf3 --- /dev/null +++ b/apps/smp-server/static/media/logo-symbol-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/smp-server/static/media/logo-symbol-light.svg b/apps/smp-server/static/media/logo-symbol-light.svg new file mode 100644 index 000000000..d8b5951a0 --- /dev/null +++ b/apps/smp-server/static/media/logo-symbol-light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/moon.svg b/apps/smp-server/static/media/moon.svg new file mode 100644 index 000000000..1054735b5 --- /dev/null +++ b/apps/smp-server/static/media/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/smp-server/static/media/qrcode.js b/apps/smp-server/static/media/qrcode.js new file mode 100644 index 000000000..c5c56eb37 --- /dev/null +++ b/apps/smp-server/static/media/qrcode.js @@ -0,0 +1 @@ +var QRCode=function(t){"use strict";var r,e=function(){return"function"==typeof Promise&&Promise.prototype&&Promise.prototype.then},n=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706],o=function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return 4*t+17},a=function(t){return n[t]},i=function(t){for(var r=0;0!==t;)r++,t>>>=1;return r},u=function(t){if("function"!=typeof t)throw new Error('"toSJISFunc" is not a valid function.');r=t},s=function(){return void 0!==r},f=function(t){return r(t)};function h(t,r){return t(r={exports:{}},r.exports),r.exports}var c=h((function(t,r){r.L={bit:1},r.M={bit:0},r.Q={bit:3},r.H={bit:2},r.isValid=function(t){return t&&void 0!==t.bit&&t.bit>=0&&t.bit<4},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"l":case"low":return r.L;case"m":case"medium":return r.M;case"q":case"quartile":return r.Q;case"h":case"high":return r.H;default:throw new Error("Unknown EC Level: "+t)}}(t)}catch(t){return e}}}));function g(){this.buffer=[],this.length=0}c.L,c.M,c.Q,c.H,c.isValid,g.prototype={get:function(t){var r=Math.floor(t/8);return 1==(this.buffer[r]>>>7-t%8&1)},put:function(t,r){for(var e=0;e>>r-e-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var r=Math.floor(this.length/8);this.buffer.length<=r&&this.buffer.push(0),t&&(this.buffer[r]|=128>>>this.length%8),this.length++}};var d=g;function l(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}l.prototype.set=function(t,r,e,n){var o=t*this.size+r;this.data[o]=e,n&&(this.reservedBit[o]=!0)},l.prototype.get=function(t,r){return this.data[t*this.size+r]},l.prototype.xor=function(t,r,e){this.data[t*this.size+r]^=e},l.prototype.isReserved=function(t,r){return this.reservedBit[t*this.size+r]};var v=l,p=h((function(t,r){var e=o;r.getRowColCoords=function(t){if(1===t)return[];for(var r=Math.floor(t/7)+2,n=e(t),o=145===n?26:2*Math.ceil((n-13)/(2*r-2)),a=[n-7],i=1;i=0&&t<=7},r.from=function(t){return r.isValid(t)?parseInt(t,10):void 0},r.getPenaltyN1=function(t){for(var r=t.size,n=0,o=0,a=0,i=null,u=null,s=0;s=5&&(n+=e+(o-5)),i=h,o=1),(h=t.get(f,s))===u?a++:(a>=5&&(n+=e+(a-5)),u=h,a=1)}o>=5&&(n+=e+(o-5)),a>=5&&(n+=e+(a-5))}return n},r.getPenaltyN2=function(t){for(var r=t.size,e=0,o=0;o=10&&(1488===n||93===n)&&e++,a=a<<1&2047|t.get(u,i),u>=10&&(1488===a||93===a)&&e++}return e*o},r.getPenaltyN4=function(t){for(var r=0,e=t.data.length,n=0;n=0;){for(var n=e[0],o=0;o0){var o=new Uint8Array(this.degree);return o.set(e,n),o}return e};var L=T,b=function(t){return!isNaN(t)&&t>=1&&t<=40},U="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+",x="(?:(?![A-Z0-9 $%*+\\-./:]|"+(U=U.replace(/u/g,"\\u"))+")(?:.|[\r\n]))+",k=new RegExp(U,"g"),F=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),S=new RegExp(x,"g"),D=new RegExp("[0-9]+","g"),Y=new RegExp("[A-Z $%*+\\-./:]+","g"),_=new RegExp("^"+U+"$"),z=new RegExp("^[0-9]+$"),H=new RegExp("^[A-Z0-9 $%*+\\-./:]+$"),J={KANJI:k,BYTE_KANJI:F,BYTE:S,NUMERIC:D,ALPHANUMERIC:Y,testKanji:function(t){return _.test(t)},testNumeric:function(t){return z.test(t)},testAlphanumeric:function(t){return H.test(t)}},K=h((function(t,r){r.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},r.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},r.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},r.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},r.MIXED={bit:-1},r.getCharCountIndicator=function(t,r){if(!t.ccBits)throw new Error("Invalid mode: "+t);if(!b(r))throw new Error("Invalid version: "+r);return r>=1&&r<10?t.ccBits[0]:r<27?t.ccBits[1]:t.ccBits[2]},r.getBestModeForData=function(t){return J.testNumeric(t)?r.NUMERIC:J.testAlphanumeric(t)?r.ALPHANUMERIC:J.testKanji(t)?r.KANJI:r.BYTE},r.toString=function(t){if(t&&t.id)return t.id;throw new Error("Invalid mode")},r.isValid=function(t){return t&&t.bit&&t.ccBits},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"numeric":return r.NUMERIC;case"alphanumeric":return r.ALPHANUMERIC;case"kanji":return r.KANJI;case"byte":return r.BYTE;default:throw new Error("Unknown mode: "+t)}}(t)}catch(t){return e}}}));K.NUMERIC,K.ALPHANUMERIC,K.BYTE,K.KANJI,K.MIXED,K.getCharCountIndicator,K.getBestModeForData,K.isValid;var O=h((function(t,r){var e=i(7973);function n(t,r){return K.getCharCountIndicator(t,r)+4}function o(t,r){var e=0;return t.forEach((function(t){var o=n(t.mode,r);e+=o+t.getBitsLength()})),e}r.from=function(t,r){return b(t)?parseInt(t,10):r},r.getCapacity=function(t,r,e){if(!b(t))throw new Error("Invalid QR Code version");void 0===e&&(e=K.BYTE);var o=8*(a(t)-M(t,r));if(e===K.MIXED)return o;var i=o-n(e,t);switch(e){case K.NUMERIC:return Math.floor(i/10*3);case K.ALPHANUMERIC:return Math.floor(i/11*2);case K.KANJI:return Math.floor(i/13);case K.BYTE:default:return Math.floor(i/8)}},r.getBestVersionForData=function(t,e){var n,a=c.from(e,c.M);if(Array.isArray(t)){if(t.length>1)return function(t,e){for(var n=1;n<=40;n++){if(o(t,n)<=r.getCapacity(n,e,K.MIXED))return n}}(t,a);if(0===t.length)return 1;n=t[0]}else n=t;return function(t,e,n){for(var o=1;o<=40;o++)if(e<=r.getCapacity(o,n,t))return o}(n.mode,n.getLength(),a)},r.getEncodedBits=function(t){if(!b(t)||t<7)throw new Error("Invalid QR Code version");for(var r=t<<12;i(r)-e>=0;)r^=7973<=0;)n^=1335<0&&(e=this.data.substr(r),n=parseInt(e,10),t.put(n,3*o+1))};var j=q,$=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function X(t){this.mode=K.ALPHANUMERIC,this.data=t}X.getBitsLength=function(t){return 11*Math.floor(t/2)+t%2*6},X.prototype.getLength=function(){return this.data.length},X.prototype.getBitsLength=function(){return X.getBitsLength(this.data.length)},X.prototype.write=function(t){var r;for(r=0;r+2<=this.data.length;r+=2){var e=45*$.indexOf(this.data[r]);e+=$.indexOf(this.data[r+1]),t.put(e,11)}this.data.length%2&&t.put($.indexOf(this.data[r]),6)};var Z=X;function W(t){this.mode=K.BYTE,"string"==typeof t&&(t=function(t){for(var r=[],e=t.length,n=0;n=55296&&o<=56319&&e>n+1){var a=t.charCodeAt(n+1);a>=56320&&a<=57343&&(o=1024*(o-55296)+a-56320+65536,n+=1)}o<128?r.push(o):o<2048?(r.push(o>>6|192),r.push(63&o|128)):o<55296||o>=57344&&o<65536?(r.push(o>>12|224),r.push(o>>6&63|128),r.push(63&o|128)):o>=65536&&o<=1114111?(r.push(o>>18|240),r.push(o>>12&63|128),r.push(o>>6&63|128),r.push(63&o|128)):r.push(239,191,189)}return new Uint8Array(r).buffer}(t)),this.data=new Uint8Array(t)}W.getBitsLength=function(t){return 8*t},W.prototype.getLength=function(){return this.data.length},W.prototype.getBitsLength=function(){return W.getBitsLength(this.data.length)},W.prototype.write=function(t){for(var r=0,e=this.data.length;r=33088&&e<=40956)e-=33088;else{if(!(e>=57408&&e<=60351))throw new Error("Invalid SJIS character: "+this.data[r]+"\nMake sure your charset is UTF-8");e-=49472}e=192*(e>>>8&255)+(255&e),t.put(e,13)}};var rt=tt,et=h((function(t){var r={single_source_shortest_paths:function(t,e,n){var o={},a={};a[e]=0;var i,u,s,f,h,c,g,d=r.PriorityQueue.make();for(d.push(e,0);!d.empty();)for(s in u=(i=d.pop()).value,f=i.cost,h=t[u]||{})h.hasOwnProperty(s)&&(c=f+h[s],g=a[s],(void 0===a[s]||g>c)&&(a[s]=c,d.push(s,c),o[s]=u));if(void 0!==n&&void 0===a[n]){var l=["Could not find a path from ",e," to ",n,"."].join("");throw new Error(l)}return o},extract_shortest_path_from_predecessor_list:function(t,r){for(var e=[],n=r;n;)e.push(n),n=t[n];return e.reverse(),e},find_path:function(t,e,n){var o=r.single_source_shortest_paths(t,e,n);return r.extract_shortest_path_from_predecessor_list(o,n)},PriorityQueue:{make:function(t){var e,n=r.PriorityQueue,o={};for(e in t=t||{},n)n.hasOwnProperty(e)&&(o[e]=n[e]);return o.queue=[],o.sorter=t.sorter||n.default_sorter,o},default_sorter:function(t,r){return t.cost-r.cost},push:function(t,r){var e={value:t,cost:r};this.queue.push(e),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return 0===this.queue.length}}};t.exports=r})),nt=h((function(t,r){function e(t){return unescape(encodeURIComponent(t)).length}function n(t,r,e){for(var n,o=[];null!==(n=t.exec(e));)o.push({data:n[0],index:n.index,mode:r,length:n[0].length});return o}function o(t){var r,e,o=n(J.NUMERIC,K.NUMERIC,t),a=n(J.ALPHANUMERIC,K.ALPHANUMERIC,t);return s()?(r=n(J.BYTE,K.BYTE,t),e=n(J.KANJI,K.KANJI,t)):(r=n(J.BYTE_KANJI,K.BYTE,t),e=[]),o.concat(a,r,e).sort((function(t,r){return t.index-r.index})).map((function(t){return{data:t.data,mode:t.mode,length:t.length}}))}function a(t,r){switch(r){case K.NUMERIC:return j.getBitsLength(t);case K.ALPHANUMERIC:return Z.getBitsLength(t);case K.KANJI:return rt.getBitsLength(t);case K.BYTE:return G.getBitsLength(t)}}function i(t,r){var e,n=K.getBestModeForData(t);if((e=K.from(r,n))!==K.BYTE&&e.bit=0?t[t.length-1]:null;return e&&e.mode===r.mode?(t[t.length-1].data+=r.data,t):(t.push(r),t)}),[])}(s))},r.rawSplit=function(t){return r.fromArray(o(t))}}));function ot(t,r,e){var n,o,a=t.size,i=V(r,e);for(n=0;n<15;n++)o=1==(i>>n&1),n<6?t.set(n,8,o,!0):n<8?t.set(n+1,8,o,!0):t.set(a-15+n,8,o,!0),n<8?t.set(8,a-n-1,o,!0):n<9?t.set(8,15-n-1+1,o,!0):t.set(8,15-n-1,o,!0);t.set(a-8,8,1,!0)}function at(t,r,e){var n=new d;e.forEach((function(r){n.put(r.mode.bit,4),n.put(r.getLength(),K.getCharCountIndicator(r.mode,t)),r.write(n)}));var o=8*(a(t)-M(t,r));for(n.getLengthInBits()+4<=o&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(0);for(var i=(o-n.getLengthInBits())/8,u=0;u=0&&u<=6&&(0===s||6===s)||s>=0&&s<=6&&(0===u||6===u)||u>=2&&u<=4&&s>=2&&s<=4?t.set(a+u,i+s,!0,!0):t.set(a+u,i+s,!1,!0))}(c,r),function(t){for(var r=t.size,e=8;e=7&&function(t,r){for(var e,n,o,a=t.size,i=O.getEncodedBits(r),u=0;u<18;u++)e=Math.floor(u/3),n=u%3+a-8-3,o=1==(i>>u&1),t.set(e,n,o,!0),t.set(n,e,o,!0)}(c,r),function(t,r){for(var e=t.size,n=-1,o=e-1,a=7,i=0,u=e-1;u>0;u-=2)for(6===u&&u--;;){for(var s=0;s<2;s++)if(!t.isReserved(o,u-s)){var f=!1;i>>a&1)),t.set(o,u-s,f),-1===--a&&(i++,a=7)}if((o+=n)<0||e<=o){o-=n,n=-n;break}}}(c,f),isNaN(n)&&(n=E.getBestMask(c,ot.bind(null,c,e))),E.applyMask(n,c),ot(c,e,n),{modules:c,version:r,errorCorrectionLevel:e,maskPattern:n,segments:a}}nt.fromArray,nt.fromString,nt.rawSplit;var ut=function(t,r){if(void 0===t||""===t)throw new Error("No input text");var e,n,o=c.M;return void 0!==r&&(o=c.from(r.errorCorrectionLevel,c.M),e=O.from(r.version),n=E.from(r.maskPattern),r.toSJISFunc&&u(r.toSJISFunc)),it(t,e,o,n)},st=h((function(t,r){function e(t){if("number"==typeof t&&(t=t.toString()),"string"!=typeof t)throw new Error("Color should be defined as hex string");var r=t.slice().replace("#","").split("");if(r.length<3||5===r.length||r.length>8)throw new Error("Invalid hex color: "+t);3!==r.length&&4!==r.length||(r=Array.prototype.concat.apply([],r.map((function(t){return[t,t]})))),6===r.length&&r.push("F","F");var e=parseInt(r.join(""),16);return{r:e>>24&255,g:e>>16&255,b:e>>8&255,a:255&e,hex:"#"+r.slice(0,6).join("")}}r.getOptions=function(t){t||(t={}),t.color||(t.color={});var r=void 0===t.margin||null===t.margin||t.margin<0?4:t.margin,n=t.width&&t.width>=21?t.width:void 0,o=t.scale||4;return{width:n,scale:n?4:o,margin:r,color:{dark:e(t.color.dark||"#000000ff"),light:e(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},r.getScale=function(t,r){return r.width&&r.width>=t+2*r.margin?r.width/(t+2*r.margin):r.scale},r.getImageWidth=function(t,e){var n=r.getScale(t,e);return Math.floor((t+2*e.margin)*n)},r.qrToImageData=function(t,e,n){for(var o=e.modules.size,a=e.modules.data,i=r.getScale(o,n),u=Math.floor((o+2*n.margin)*i),s=n.margin*i,f=[n.color.light,n.color.dark],h=0;h=s&&c>=s&&h':"",s="0&&s>0&&t[u-1]||(n+=a?ct("M",s+e,.5+f+e):ct("m",o,0),o=0,a=!1),s+1',f='viewBox="0 0 '+i+" "+i+'"',h=''+u+s+"\n";return"function"==typeof e&&e(null,h),h};function dt(t,r,n,o,a){var i=[].slice.call(arguments,1),u=i.length,s="function"==typeof i[u-1];if(!s&&!e())throw new Error("Callback required as last argument");if(!s){if(u<1)throw new Error("Too few arguments provided");return 1===u?(n=r,r=o=void 0):2!==u||r.getContext||(o=n,n=r,r=void 0),new Promise((function(e,a){try{var i=ut(n,o);e(t(i,r,o))}catch(t){a(t)}}))}if(u<2)throw new Error("Too few arguments provided");2===u?(a=n,n=r,r=o=void 0):3===u&&(r.getContext&&void 0===a?(a=o,o=void 0):(a=o,o=n,n=r,r=void 0));try{var f=ut(n,o);a(null,t(f,r,o))}catch(t){a(t)}}var lt=ut,vt=dt.bind(null,ft.render),pt=dt.bind(null,ft.renderToDataURL),wt=dt.bind(null,(function(t,r,e){return gt(t,e)})),mt={create:lt,toCanvas:vt,toDataURL:pt,toString:wt};return t.create=lt,t.default=mt,t.toCanvas=vt,t.toDataURL=pt,t.toString=wt,Object.defineProperty(t,"__esModule",{value:!0}),t}({}); diff --git a/apps/smp-server/static/media/script.js b/apps/smp-server/static/media/script.js new file mode 100644 index 000000000..aa5d35293 --- /dev/null +++ b/apps/smp-server/static/media/script.js @@ -0,0 +1,39 @@ +const isMobile = { + Android: () => navigator.userAgent.match(/Android/i), + iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i) +}; + +window.addEventListener('click', clickHandler) + +if (isMobile.iOS) { + for (const btn of document.getElementsByClassName("close-overlay-btn")) { + btn.addEventListener("touchend", (e) => setTimeout(() => closeOverlay(e), 100)) + } +} + +function clickHandler(e) { + if (e.target.closest('.contact-tab-btn')) { + e.target.closest('.contact-tab').classList.toggle('active') + } +} + +window.addEventListener('load', () => { + const googlePlayBtn = document.querySelector('.google-play-btn'); + const appleStoreBtn = document.querySelector('.apple-store-btn'); + const fDroidBtn = document.querySelector('.f-droid-btn'); + if (!googlePlayBtn || !appleStoreBtn || !fDroidBtn) return; + + + if (isMobile.Android()) { + googlePlayBtn.classList.remove('hidden'); + fDroidBtn.classList.remove('hidden'); + } + else if (isMobile.iOS()) { + appleStoreBtn.classList.remove('hidden'); + } + else { + appleStoreBtn.classList.remove('hidden'); + googlePlayBtn.classList.remove('hidden'); + fDroidBtn.classList.remove('hidden'); + } +}) diff --git a/apps/smp-server/static/media/style.css b/apps/smp-server/static/media/style.css new file mode 100644 index 000000000..d9d2e829e --- /dev/null +++ b/apps/smp-server/static/media/style.css @@ -0,0 +1,410 @@ +@font-face { + font-family: Gilroy; + src: url("GilroyRegular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyLight.woff2") format("woff2"); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyMedium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyBold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyRegularItalic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; +} + +html { + scroll-behavior: smooth; + font-family: Gilroy, Helvetica, sans-serif; + ; + letter-spacing: 0.003em; +} + +img { + user-select: none; + -webkit-user-select: none; + /* For Safari and older Chrome versions */ + -moz-user-select: none; + /* For Firefox */ + -ms-user-select: none; + /* For Internet Explorer and Edge */ +} + +/* NEW SITE */ +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + width: 100%; + /* padding: 0 20px; */ + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + + .container-sm, + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + + .container-md, + .container-sm, + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + + .container-lg, + .container-md, + .container-sm, + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + + .container-xl, + .container-lg, + .container-md, + .container-sm, + .container { + max-width: 1140px; + } +} + +@media (min-width: 1400px) { + + .container-xxl, + .container-xl, + .container-lg, + .container-md, + .container-sm, + .container { + max-width: 1320px; + } +} + +.gradient-text { + background: -webkit-linear-gradient(to bottom, #53C1FF -50%, #0053D0 160%); + background: linear-gradient(to bottom, #53C1FF -50%, #0053D0 160%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + +.dark .border-gradient { + background: + linear-gradient(#11182F, #11182F) padding-box, + linear-gradient(to bottom, transparent, #01F1FF 58%) border-box; + border: 1px solid transparent; +} + +.dark .only-light { + display: none; +} + +.only-dark { + display: none; +} + +.dark .only-dark { + display: inherit; +} + +.menu-link { + font-size: 16px; + line-height: 33.42px; + color: #0D0E12; +} + +.dark .menu-link { + color: #fff; +} + +.nav-link ul li a.active { + color: #0053D0; + +} + +.dark .nav-link ul li a.active { + color: #66D9E2; +} + +@media (min-width:1024px) { + + .nav-link-text, + .menu-link { + display: inline-block; + position: relative; + color: #0D0E12; + } + + .nav-link-text::before, + .active .nav-link-text::before, + .menu-link::before { + content: ""; + position: absolute; + width: 0; + height: 1px; + bottom: 0; + right: 0; + /* background-color: initial; */ + transition: width 0.25s ease-out; + } + + .menu-link::before { + background-color: #0D0E12; + } + + .dark .menu-link::before { + background-color: #fff; + } + + .active .nav-link-text::before { + width: 100%; + } + + .nav-link:hover .nav-link-text::before, + .menu-link:hover::before { + width: 100%; + left: 0; + right: auto; + } +} + + +.sub-menu { + visibility: hidden; + opacity: 0; + color: #505158; +} + +.sub-menu .no-hover { + color: #505158 !important; +} + +.dark .sub-menu, +.dark .sub-menu .no-hover { + color: #fff !important; +} + +.dark .sub-menu li:hover { + color: #66D9E2; +} + +.sub-menu li:hover { + color: #0053D0; +} + +.sub-menu { + transition: all .3s ease !important; +} + +.nav-link span svg, +header nav { + transition: all 0.5s ease; +} + +.nav-link:hover span svg { + transform: rotate(180deg); +} + +@media (min-width:1024px) { + + .nav-link:hover .sub-menu, + .nav-link:focus-within .sub-menu { + visibility: visible; + opacity: 1; + margin-top: 0; + } +} + +@media (max-width: 1024px) { + .sub-menu { + max-height: 0; + transform: translateY(-10px); + transition: all .7s ease !important; + } + + .active .sub-menu { + max-height: 600px; + transform: translateY(0px); + opacity: 1; + visibility: visible; + margin-top: 0; + } + + header nav { + visibility: hidden; + opacity: 0; + transform: translateX(100%); + } + + header nav.open { + visibility: visible; + opacity: 1; + transform: translateX(0); + } +} + +.lock-scroll { + overflow: hidden; +} + +/* hero */ +header { + transition: all .7s ease; +} + +.primary-header { + background: linear-gradient(270deg, #0053D0 35.85%, #0197FF 94.78%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .primary-header { + background: linear-gradient(270deg, #70F0F9 100%, #70F0F9 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.secondary-header { + color: #606c71; + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .secondary-header { + color: #fff; + text-shadow: none; +} + +.description { + width: 31rem; +} + +p a { + color: #0053D0; + text-decoration: underline; + text-underline-offset: 2px; +} + +.dark p a { + color: #70F0F9; +} + +/* For Contact & Invitation Page */ +.primary-header-contact { + background: linear-gradient(251.16deg, #53c1ff 1.1%, #0053d0 100.82%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .primary-header-contact { + background: linear-gradient(270deg, #70F0F9 100%, #70F0F9 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.secondary-header-contact { + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .secondary-header-contact { + text-shadow: none; +} + +.content_copy_with_tooltip { + background-color: #f8f8f6; + border-radius: 50px; + padding-bottom: 4px; + padding-top: 8px; + margin-top: 16px; + margin-bottom: 16px; +} + +.content_copy_with_tooltip .tooltip { + vertical-align: -6px; +} + +.content_copy_with_tooltip .content { + font-size: 15px; +} + +.contact-tab>.contact-tab-content, +.job-tab>.job-tab-content { + opacity: 0; + max-height: 0; + transition: all 0.5s ease; + visibility: hidden; + transform: translateY(10px); + overflow: hidden; +} + +.contact-tab svg, +.job-tab svg { + transform: rotate(-180deg); + transition: all .5s ease; +} + +.contact-tab.active>.contact-tab-content, +.job-tab.active>.job-tab-content { + opacity: 1; + max-height: 300px; + visibility: visible; + transform: translateY(0px); +} + +.for-tablet .contact-tab.active>.contact-tab-content, +.for-tablet .job-tab.active>.job-tab-content { + min-height: 450px; +} + +.contact-tab.active svg, +.contact-tab:hover svg, +.job-tab.active svg, +.job-tab:hover svg { + transform: rotate(0deg); +} + +.d-none-if-js-disabled { + display: none !important; +} \ No newline at end of file diff --git a/apps/smp-server/static/media/sun.svg b/apps/smp-server/static/media/sun.svg new file mode 100644 index 000000000..8407b98e5 --- /dev/null +++ b/apps/smp-server/static/media/sun.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/smp-server/static/media/swiper-bundle.min.css b/apps/smp-server/static/media/swiper-bundle.min.css new file mode 100644 index 000000000..916173ac0 --- /dev/null +++ b/apps/smp-server/static/media/swiper-bundle.min.css @@ -0,0 +1,13 @@ +/** + * Swiper 8.4.3 + * Most modern mobile touch slider and framework with hardware accelerated transitions + * https://swiperjs.com + * + * Copyright 2014-2022 Vladimir Kharlampidi + * + * Released under the MIT License + * + * Released on: October 6, 2022 + */ + +@font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, 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');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;box-sizing:content-box}.swiper-android .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-pointer-events{touch-action:pan-y}.swiper-pointer-events.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d,.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-cube-shadow,.swiper-3d .swiper-slide,.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top,.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-horizontal.swiper-css-mode>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-vertical.swiper-css-mode>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-centered>.swiper-wrapper::before{content:'';flex-shrink:0;order:9999}.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-centered.swiper-horizontal>.swiper-wrapper::before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-centered.swiper-vertical>.swiper-wrapper::before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center}.swiper-virtual .swiper-slide{-webkit-backface-visibility:hidden;transform:translateZ(0)}.swiper-virtual.swiper-css-mode .swiper-wrapper::after{content:'';position:absolute;left:0;top:0;pointer-events:none}.swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after{height:1px;width:var(--swiper-virtual-size)}.swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after{width:1px;height:var(--swiper-virtual-size)}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:50%;width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:initial;line-height:1}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:10px;right:auto}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:10px;left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:'next'}.swiper-button-lock{display:none}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-pagination-disabled>.swiper-pagination,.swiper-pagination.swiper-pagination-disabled{display:none!important}.swiper-horizontal>.swiper-pagination-bullets,.swiper-pagination-bullets.swiper-pagination-horizontal,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:10px;left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:var(--swiper-pagination-bullet-width,var(--swiper-pagination-bullet-size,8px));height:var(--swiper-pagination-bullet-height,var(--swiper-pagination-bullet-size,8px));display:inline-block;border-radius:50%;background:var(--swiper-pagination-bullet-inactive-color,#000);opacity:var(--swiper-pagination-bullet-inactive-opacity, .2)}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:var(--swiper-pagination-bullet-opacity, 1);background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-pagination-vertical.swiper-pagination-bullets,.swiper-vertical>.swiper-pagination-bullets{right:10px;top:50%;transform:translate3d(0px,-50%,0)}.swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:var(--swiper-pagination-bullet-vertical-gap,6px) 0;display:block}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 var(--swiper-pagination-bullet-horizontal-gap,4px)}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-horizontal.swiper-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-progressbar{background:rgba(0,0,0,.25);position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-horizontal>.swiper-pagination-progressbar,.swiper-pagination-progressbar.swiper-pagination-horizontal,.swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite,.swiper-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:4px;left:0;top:0}.swiper-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-vertical,.swiper-vertical>.swiper-pagination-progressbar{width:4px;height:100%;left:0;top:0}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:10px;position:relative;-ms-touch-action:none;background:rgba(0,0,0,.1)}.swiper-scrollbar-disabled>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-disabled{display:none!important}.swiper-horizontal>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-horizontal{position:absolute;left:1%;bottom:3px;z-index:50;height:5px;width:98%}.swiper-scrollbar.swiper-scrollbar-vertical,.swiper-vertical>.swiper-scrollbar{position:absolute;right:3px;top:1%;z-index:50;width:5px;height:98%}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:rgba(0,0,0,.5);border-radius:10px;left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader,.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.swiper .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-grid>.swiper-wrapper{flex-wrap:wrap}.swiper-grid-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-fade.swiper-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-fade .swiper-slide-active,.swiper-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube{overflow:visible}.swiper-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-cube.swiper-rtl .swiper-slide{transform-origin:100% 0}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-next,.swiper-cube .swiper-slide-next+.swiper-slide,.swiper-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-cube .swiper-slide-shadow-bottom,.swiper-cube .swiper-slide-shadow-left,.swiper-cube .swiper-slide-shadow-right,.swiper-cube .swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-flip{overflow:visible}.swiper-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-flip .swiper-slide-active,.swiper-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-flip .swiper-slide-shadow-bottom,.swiper-flip .swiper-slide-shadow-left,.swiper-flip .swiper-slide-shadow-right,.swiper-flip .swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-creative .swiper-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;transition-property:transform,opacity,height}.swiper-cards{overflow:visible}.swiper-cards .swiper-slide{transform-origin:center bottom;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden} \ No newline at end of file diff --git a/apps/smp-server/static/media/swiper-bundle.min.js b/apps/smp-server/static/media/swiper-bundle.min.js new file mode 100644 index 000000000..0c347e3e0 --- /dev/null +++ b/apps/smp-server/static/media/swiper-bundle.min.js @@ -0,0 +1,14 @@ +/** + * Swiper 8.4.3 + * Most modern mobile touch slider and framework with hardware accelerated transitions + * https://swiperjs.com + * + * Copyright 2014-2022 Vladimir Kharlampidi + * + * Released under the MIT License + * + * Released on: October 6, 2022 + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Swiper=t()}(this,(function(){"use strict";function e(e){return null!==e&&"object"==typeof e&&"constructor"in e&&e.constructor===Object}function t(s,a){void 0===s&&(s={}),void 0===a&&(a={}),Object.keys(a).forEach((i=>{void 0===s[i]?s[i]=a[i]:e(a[i])&&e(s[i])&&Object.keys(a[i]).length>0&&t(s[i],a[i])}))}const s={body:{},addEventListener(){},removeEventListener(){},activeElement:{blur(){},nodeName:""},querySelector:()=>null,querySelectorAll:()=>[],getElementById:()=>null,createEvent:()=>({initEvent(){}}),createElement:()=>({children:[],childNodes:[],style:{},setAttribute(){},getElementsByTagName:()=>[]}),createElementNS:()=>({}),importNode:()=>null,location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""}};function a(){const e="undefined"!=typeof document?document:{};return t(e,s),e}const i={document:s,navigator:{userAgent:""},location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""},history:{replaceState(){},pushState(){},go(){},back(){}},CustomEvent:function(){return this},addEventListener(){},removeEventListener(){},getComputedStyle:()=>({getPropertyValue:()=>""}),Image(){},Date(){},screen:{},setTimeout(){},clearTimeout(){},matchMedia:()=>({}),requestAnimationFrame:e=>"undefined"==typeof setTimeout?(e(),null):setTimeout(e,0),cancelAnimationFrame(e){"undefined"!=typeof setTimeout&&clearTimeout(e)}};function r(){const e="undefined"!=typeof window?window:{};return t(e,i),e}class n extends Array{constructor(e){"number"==typeof e?super(e):(super(...e||[]),function(e){const t=e.__proto__;Object.defineProperty(e,"__proto__",{get:()=>t,set(e){t.__proto__=e}})}(this))}}function l(e){void 0===e&&(e=[]);const t=[];return e.forEach((e=>{Array.isArray(e)?t.push(...l(e)):t.push(e)})),t}function o(e,t){return Array.prototype.filter.call(e,t)}function d(e,t){const s=r(),i=a();let l=[];if(!t&&e instanceof n)return e;if(!e)return new n(l);if("string"==typeof e){const s=e.trim();if(s.indexOf("<")>=0&&s.indexOf(">")>=0){let e="div";0===s.indexOf("e.split(" "))));return this.forEach((e=>{e.classList.add(...a)})),this},removeClass:function(){for(var e=arguments.length,t=new Array(e),s=0;se.split(" "))));return this.forEach((e=>{e.classList.remove(...a)})),this},hasClass:function(){for(var e=arguments.length,t=new Array(e),s=0;se.split(" "))));return o(this,(e=>a.filter((t=>e.classList.contains(t))).length>0)).length>0},toggleClass:function(){for(var e=arguments.length,t=new Array(e),s=0;se.split(" "))));this.forEach((e=>{a.forEach((t=>{e.classList.toggle(t)}))}))},attr:function(e,t){if(1===arguments.length&&"string"==typeof e)return this[0]?this[0].getAttribute(e):void 0;for(let s=0;s=0;e-=1){const i=a[e];r&&i.listener===r||r&&i.listener&&i.listener.dom7proxy&&i.listener.dom7proxy===r?(s.removeEventListener(t,i.proxyListener,n),a.splice(e,1)):r||(s.removeEventListener(t,i.proxyListener,n),a.splice(e,1))}}}return this},trigger:function(){const e=r();for(var t=arguments.length,s=new Array(t),a=0;at>0)),i.dispatchEvent(t),i.dom7EventData=[],delete i.dom7EventData}}}return this},transitionEnd:function(e){const t=this;return e&&t.on("transitionend",(function s(a){a.target===this&&(e.call(this,a),t.off("transitionend",s))})),this},outerWidth:function(e){if(this.length>0){if(e){const e=this.styles();return this[0].offsetWidth+parseFloat(e.getPropertyValue("margin-right"))+parseFloat(e.getPropertyValue("margin-left"))}return this[0].offsetWidth}return null},outerHeight:function(e){if(this.length>0){if(e){const e=this.styles();return this[0].offsetHeight+parseFloat(e.getPropertyValue("margin-top"))+parseFloat(e.getPropertyValue("margin-bottom"))}return this[0].offsetHeight}return null},styles:function(){const e=r();return this[0]?e.getComputedStyle(this[0],null):{}},offset:function(){if(this.length>0){const e=r(),t=a(),s=this[0],i=s.getBoundingClientRect(),n=t.body,l=s.clientTop||n.clientTop||0,o=s.clientLeft||n.clientLeft||0,d=s===e?e.scrollY:s.scrollTop,c=s===e?e.scrollX:s.scrollLeft;return{top:i.top+d-l,left:i.left+c-o}}return null},css:function(e,t){const s=r();let a;if(1===arguments.length){if("string"!=typeof e){for(a=0;a{e.apply(t,[t,s])})),this):this},html:function(e){if(void 0===e)return this[0]?this[0].innerHTML:null;for(let t=0;tt-1)return d([]);if(e<0){const s=t+e;return d(s<0?[]:[this[s]])}return d([this[e]])},append:function(){let e;const t=a();for(let s=0;s=0;i-=1)this[s].insertBefore(a.childNodes[i],this[s].childNodes[0])}else if(e instanceof n)for(i=0;i0?e?this[0].nextElementSibling&&d(this[0].nextElementSibling).is(e)?d([this[0].nextElementSibling]):d([]):this[0].nextElementSibling?d([this[0].nextElementSibling]):d([]):d([])},nextAll:function(e){const t=[];let s=this[0];if(!s)return d([]);for(;s.nextElementSibling;){const a=s.nextElementSibling;e?d(a).is(e)&&t.push(a):t.push(a),s=a}return d(t)},prev:function(e){if(this.length>0){const t=this[0];return e?t.previousElementSibling&&d(t.previousElementSibling).is(e)?d([t.previousElementSibling]):d([]):t.previousElementSibling?d([t.previousElementSibling]):d([])}return d([])},prevAll:function(e){const t=[];let s=this[0];if(!s)return d([]);for(;s.previousElementSibling;){const a=s.previousElementSibling;e?d(a).is(e)&&t.push(a):t.push(a),s=a}return d(t)},parent:function(e){const t=[];for(let s=0;s6&&(i=i.split(", ").map((e=>e.replace(",","."))).join(", ")),n=new s.WebKitCSSMatrix("none"===i?"":i)):(n=l.MozTransform||l.OTransform||l.MsTransform||l.msTransform||l.transform||l.getPropertyValue("transform").replace("translate(","matrix(1, 0, 0, 1,"),a=n.toString().split(",")),"x"===t&&(i=s.WebKitCSSMatrix?n.m41:16===a.length?parseFloat(a[12]):parseFloat(a[4])),"y"===t&&(i=s.WebKitCSSMatrix?n.m42:16===a.length?parseFloat(a[13]):parseFloat(a[5])),i||0}function m(e){return"object"==typeof e&&null!==e&&e.constructor&&"Object"===Object.prototype.toString.call(e).slice(8,-1)}function f(e){return"undefined"!=typeof window&&void 0!==window.HTMLElement?e instanceof HTMLElement:e&&(1===e.nodeType||11===e.nodeType)}function g(){const e=Object(arguments.length<=0?void 0:arguments[0]),t=["__proto__","constructor","prototype"];for(let s=1;st.indexOf(e)<0));for(let t=0,i=s.length;tn?"next":"prev",p=(e,t)=>"next"===c&&e>=t||"prev"===c&&e<=t,u=()=>{l=(new Date).getTime(),null===o&&(o=l);const e=Math.max(Math.min((l-o)/d,1),0),r=.5-Math.cos(e*Math.PI)/2;let c=n+r*(s-n);if(p(c,s)&&(c=s),t.wrapperEl.scrollTo({[a]:c}),p(c,s))return t.wrapperEl.style.overflow="hidden",t.wrapperEl.style.scrollSnapType="",setTimeout((()=>{t.wrapperEl.style.overflow="",t.wrapperEl.scrollTo({[a]:c})})),void i.cancelAnimationFrame(t.cssModeFrameID);t.cssModeFrameID=i.requestAnimationFrame(u)};u()}let b,x,y;function E(){return b||(b=function(){const e=r(),t=a();return{smoothScroll:t.documentElement&&"scrollBehavior"in t.documentElement.style,touch:!!("ontouchstart"in e||e.DocumentTouch&&t instanceof e.DocumentTouch),passiveListener:function(){let t=!1;try{const s=Object.defineProperty({},"passive",{get(){t=!0}});e.addEventListener("testPassiveListener",null,s)}catch(e){}return t}(),gestures:"ongesturestart"in e}}()),b}function C(e){return void 0===e&&(e={}),x||(x=function(e){let{userAgent:t}=void 0===e?{}:e;const s=E(),a=r(),i=a.navigator.platform,n=t||a.navigator.userAgent,l={ios:!1,android:!1},o=a.screen.width,d=a.screen.height,c=n.match(/(Android);?[\s\/]+([\d.]+)?/);let p=n.match(/(iPad).*OS\s([\d_]+)/);const u=n.match(/(iPod)(.*OS\s([\d_]+))?/),h=!p&&n.match(/(iPhone\sOS|iOS)\s([\d_]+)/),m="Win32"===i;let f="MacIntel"===i;return!p&&f&&s.touch&&["1024x1366","1366x1024","834x1194","1194x834","834x1112","1112x834","768x1024","1024x768","820x1180","1180x820","810x1080","1080x810"].indexOf(`${o}x${d}`)>=0&&(p=n.match(/(Version)\/([\d.]+)/),p||(p=[0,1,"13_0_0"]),f=!1),c&&!m&&(l.os="android",l.android=!0),(p||h||u)&&(l.os="ios",l.ios=!0),l}(e)),x}function T(){return y||(y=function(){const e=r();return{isSafari:function(){const t=e.navigator.userAgent.toLowerCase();return t.indexOf("safari")>=0&&t.indexOf("chrome")<0&&t.indexOf("android")<0}(),isWebView:/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(e.navigator.userAgent)}}()),y}Object.keys(c).forEach((e=>{Object.defineProperty(d.fn,e,{value:c[e],writable:!0})}));var $={on(e,t,s){const a=this;if(!a.eventsListeners||a.destroyed)return a;if("function"!=typeof t)return a;const i=s?"unshift":"push";return e.split(" ").forEach((e=>{a.eventsListeners[e]||(a.eventsListeners[e]=[]),a.eventsListeners[e][i](t)})),a},once(e,t,s){const a=this;if(!a.eventsListeners||a.destroyed)return a;if("function"!=typeof t)return a;function i(){a.off(e,i),i.__emitterProxy&&delete i.__emitterProxy;for(var s=arguments.length,r=new Array(s),n=0;n=0&&t.eventsAnyListeners.splice(s,1),t},off(e,t){const s=this;return!s.eventsListeners||s.destroyed?s:s.eventsListeners?(e.split(" ").forEach((e=>{void 0===t?s.eventsListeners[e]=[]:s.eventsListeners[e]&&s.eventsListeners[e].forEach(((a,i)=>{(a===t||a.__emitterProxy&&a.__emitterProxy===t)&&s.eventsListeners[e].splice(i,1)}))})),s):s},emit(){const e=this;if(!e.eventsListeners||e.destroyed)return e;if(!e.eventsListeners)return e;let t,s,a;for(var i=arguments.length,r=new Array(i),n=0;n{e.eventsAnyListeners&&e.eventsAnyListeners.length&&e.eventsAnyListeners.forEach((e=>{e.apply(a,[t,...s])})),e.eventsListeners&&e.eventsListeners[t]&&e.eventsListeners[t].forEach((e=>{e.apply(a,s)}))})),e}};var S={updateSize:function(){const e=this;let t,s;const a=e.$el;t=void 0!==e.params.width&&null!==e.params.width?e.params.width:a[0].clientWidth,s=void 0!==e.params.height&&null!==e.params.height?e.params.height:a[0].clientHeight,0===t&&e.isHorizontal()||0===s&&e.isVertical()||(t=t-parseInt(a.css("padding-left")||0,10)-parseInt(a.css("padding-right")||0,10),s=s-parseInt(a.css("padding-top")||0,10)-parseInt(a.css("padding-bottom")||0,10),Number.isNaN(t)&&(t=0),Number.isNaN(s)&&(s=0),Object.assign(e,{width:t,height:s,size:e.isHorizontal()?t:s}))},updateSlides:function(){const e=this;function t(t){return e.isHorizontal()?t:{width:"height","margin-top":"margin-left","margin-bottom ":"margin-right","margin-left":"margin-top","margin-right":"margin-bottom","padding-left":"padding-top","padding-right":"padding-bottom",marginRight:"marginBottom"}[t]}function s(e,s){return parseFloat(e.getPropertyValue(t(s))||0)}const a=e.params,{$wrapperEl:i,size:r,rtlTranslate:n,wrongRTL:l}=e,o=e.virtual&&a.virtual.enabled,d=o?e.virtual.slides.length:e.slides.length,c=i.children(`.${e.params.slideClass}`),p=o?e.virtual.slides.length:c.length;let u=[];const h=[],m=[];let f=a.slidesOffsetBefore;"function"==typeof f&&(f=a.slidesOffsetBefore.call(e));let g=a.slidesOffsetAfter;"function"==typeof g&&(g=a.slidesOffsetAfter.call(e));const w=e.snapGrid.length,b=e.slidesGrid.length;let x=a.spaceBetween,y=-f,E=0,C=0;if(void 0===r)return;"string"==typeof x&&x.indexOf("%")>=0&&(x=parseFloat(x.replace("%",""))/100*r),e.virtualSize=-x,n?c.css({marginLeft:"",marginBottom:"",marginTop:""}):c.css({marginRight:"",marginBottom:"",marginTop:""}),a.centeredSlides&&a.cssMode&&(v(e.wrapperEl,"--swiper-centered-offset-before",""),v(e.wrapperEl,"--swiper-centered-offset-after",""));const T=a.grid&&a.grid.rows>1&&e.grid;let $;T&&e.grid.initSlides(p);const S="auto"===a.slidesPerView&&a.breakpoints&&Object.keys(a.breakpoints).filter((e=>void 0!==a.breakpoints[e].slidesPerView)).length>0;for(let i=0;i1&&u.push(e.virtualSize-r)}if(0===u.length&&(u=[0]),0!==a.spaceBetween){const s=e.isHorizontal()&&n?"marginLeft":t("marginRight");c.filter(((e,t)=>!a.cssMode||t!==c.length-1)).css({[s]:`${x}px`})}if(a.centeredSlides&&a.centeredSlidesBounds){let e=0;m.forEach((t=>{e+=t+(a.spaceBetween?a.spaceBetween:0)})),e-=a.spaceBetween;const t=e-r;u=u.map((e=>e<0?-f:e>t?t+g:e))}if(a.centerInsufficientSlides){let e=0;if(m.forEach((t=>{e+=t+(a.spaceBetween?a.spaceBetween:0)})),e-=a.spaceBetween,e{u[s]=e-t})),h.forEach(((e,s)=>{h[s]=e+t}))}}if(Object.assign(e,{slides:c,snapGrid:u,slidesGrid:h,slidesSizesGrid:m}),a.centeredSlides&&a.cssMode&&!a.centeredSlidesBounds){v(e.wrapperEl,"--swiper-centered-offset-before",-u[0]+"px"),v(e.wrapperEl,"--swiper-centered-offset-after",e.size/2-m[m.length-1]/2+"px");const t=-e.snapGrid[0],s=-e.slidesGrid[0];e.snapGrid=e.snapGrid.map((e=>e+t)),e.slidesGrid=e.slidesGrid.map((e=>e+s))}if(p!==d&&e.emit("slidesLengthChange"),u.length!==w&&(e.params.watchOverflow&&e.checkOverflow(),e.emit("snapGridLengthChange")),h.length!==b&&e.emit("slidesGridLengthChange"),a.watchSlidesProgress&&e.updateSlidesOffset(),!(o||a.cssMode||"slide"!==a.effect&&"fade"!==a.effect)){const t=`${a.containerModifierClass}backface-hidden`,s=e.$el.hasClass(t);p<=a.maxBackfaceHiddenSlides?s||e.$el.addClass(t):s&&e.$el.removeClass(t)}},updateAutoHeight:function(e){const t=this,s=[],a=t.virtual&&t.params.virtual.enabled;let i,r=0;"number"==typeof e?t.setTransition(e):!0===e&&t.setTransition(t.params.speed);const n=e=>a?t.slides.filter((t=>parseInt(t.getAttribute("data-swiper-slide-index"),10)===e))[0]:t.slides.eq(e)[0];if("auto"!==t.params.slidesPerView&&t.params.slidesPerView>1)if(t.params.centeredSlides)(t.visibleSlides||d([])).each((e=>{s.push(e)}));else for(i=0;it.slides.length&&!a)break;s.push(n(e))}else s.push(n(t.activeIndex));for(i=0;ir?e:r}(r||0===r)&&t.$wrapperEl.css("height",`${r}px`)},updateSlidesOffset:function(){const e=this,t=e.slides;for(let s=0;s=0&&p1&&u<=t.size||p<=0&&u>=t.size)&&(t.visibleSlides.push(l),t.visibleSlidesIndexes.push(e),a.eq(e).addClass(s.slideVisibleClass)),l.progress=i?-d:d,l.originalProgress=i?-c:c}t.visibleSlides=d(t.visibleSlides)},updateProgress:function(e){const t=this;if(void 0===e){const s=t.rtlTranslate?-1:1;e=t&&t.translate&&t.translate*s||0}const s=t.params,a=t.maxTranslate()-t.minTranslate();let{progress:i,isBeginning:r,isEnd:n}=t;const l=r,o=n;0===a?(i=0,r=!0,n=!0):(i=(e-t.minTranslate())/a,r=i<=0,n=i>=1),Object.assign(t,{progress:i,isBeginning:r,isEnd:n}),(s.watchSlidesProgress||s.centeredSlides&&s.autoHeight)&&t.updateSlidesProgress(e),r&&!l&&t.emit("reachBeginning toEdge"),n&&!o&&t.emit("reachEnd toEdge"),(l&&!r||o&&!n)&&t.emit("fromEdge"),t.emit("progress",i)},updateSlidesClasses:function(){const e=this,{slides:t,params:s,$wrapperEl:a,activeIndex:i,realIndex:r}=e,n=e.virtual&&s.virtual.enabled;let l;t.removeClass(`${s.slideActiveClass} ${s.slideNextClass} ${s.slidePrevClass} ${s.slideDuplicateActiveClass} ${s.slideDuplicateNextClass} ${s.slideDuplicatePrevClass}`),l=n?e.$wrapperEl.find(`.${s.slideClass}[data-swiper-slide-index="${i}"]`):t.eq(i),l.addClass(s.slideActiveClass),s.loop&&(l.hasClass(s.slideDuplicateClass)?a.children(`.${s.slideClass}:not(.${s.slideDuplicateClass})[data-swiper-slide-index="${r}"]`).addClass(s.slideDuplicateActiveClass):a.children(`.${s.slideClass}.${s.slideDuplicateClass}[data-swiper-slide-index="${r}"]`).addClass(s.slideDuplicateActiveClass));let o=l.nextAll(`.${s.slideClass}`).eq(0).addClass(s.slideNextClass);s.loop&&0===o.length&&(o=t.eq(0),o.addClass(s.slideNextClass));let d=l.prevAll(`.${s.slideClass}`).eq(0).addClass(s.slidePrevClass);s.loop&&0===d.length&&(d=t.eq(-1),d.addClass(s.slidePrevClass)),s.loop&&(o.hasClass(s.slideDuplicateClass)?a.children(`.${s.slideClass}:not(.${s.slideDuplicateClass})[data-swiper-slide-index="${o.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicateNextClass):a.children(`.${s.slideClass}.${s.slideDuplicateClass}[data-swiper-slide-index="${o.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicateNextClass),d.hasClass(s.slideDuplicateClass)?a.children(`.${s.slideClass}:not(.${s.slideDuplicateClass})[data-swiper-slide-index="${d.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicatePrevClass):a.children(`.${s.slideClass}.${s.slideDuplicateClass}[data-swiper-slide-index="${d.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicatePrevClass)),e.emitSlidesClasses()},updateActiveIndex:function(e){const t=this,s=t.rtlTranslate?t.translate:-t.translate,{slidesGrid:a,snapGrid:i,params:r,activeIndex:n,realIndex:l,snapIndex:o}=t;let d,c=e;if(void 0===c){for(let e=0;e=a[e]&&s=a[e]&&s=a[e]&&(c=e);r.normalizeSlideIndex&&(c<0||void 0===c)&&(c=0)}if(i.indexOf(s)>=0)d=i.indexOf(s);else{const e=Math.min(r.slidesPerGroupSkip,c);d=e+Math.floor((c-e)/r.slidesPerGroup)}if(d>=i.length&&(d=i.length-1),c===n)return void(d!==o&&(t.snapIndex=d,t.emit("snapIndexChange")));const p=parseInt(t.slides.eq(c).attr("data-swiper-slide-index")||c,10);Object.assign(t,{snapIndex:d,realIndex:p,previousIndex:n,activeIndex:c}),t.emit("activeIndexChange"),t.emit("snapIndexChange"),l!==p&&t.emit("realIndexChange"),(t.initialized||t.params.runCallbacksOnInit)&&t.emit("slideChange")},updateClickedSlide:function(e){const t=this,s=t.params,a=d(e).closest(`.${s.slideClass}`)[0];let i,r=!1;if(a)for(let e=0;eo?o:a&&en?"next":r=o.length&&(g=o.length-1);const v=-o[g];if(l.normalizeSlideIndex)for(let e=0;e=s&&t=s&&t=s&&(n=e)}if(r.initialized&&n!==p){if(!r.allowSlideNext&&vr.translate&&v>r.maxTranslate()&&(p||0)!==n)return!1}let b;if(n!==(c||0)&&s&&r.emit("beforeSlideChangeStart"),r.updateProgress(v),b=n>p?"next":n{r.wrapperEl.style.scrollSnapType="",r._swiperImmediateVirtual=!1}))}else{if(!r.support.smoothScroll)return w({swiper:r,targetPosition:s,side:e?"left":"top"}),!0;h.scrollTo({[e?"left":"top"]:s,behavior:"smooth"})}return!0}return r.setTransition(t),r.setTranslate(v),r.updateActiveIndex(n),r.updateSlidesClasses(),r.emit("beforeTransitionStart",t,a),r.transitionStart(s,b),0===t?r.transitionEnd(s,b):r.animating||(r.animating=!0,r.onSlideToWrapperTransitionEnd||(r.onSlideToWrapperTransitionEnd=function(e){r&&!r.destroyed&&e.target===this&&(r.$wrapperEl[0].removeEventListener("transitionend",r.onSlideToWrapperTransitionEnd),r.$wrapperEl[0].removeEventListener("webkitTransitionEnd",r.onSlideToWrapperTransitionEnd),r.onSlideToWrapperTransitionEnd=null,delete r.onSlideToWrapperTransitionEnd,r.transitionEnd(s,b))}),r.$wrapperEl[0].addEventListener("transitionend",r.onSlideToWrapperTransitionEnd),r.$wrapperEl[0].addEventListener("webkitTransitionEnd",r.onSlideToWrapperTransitionEnd)),!0},slideToLoop:function(e,t,s,a){if(void 0===e&&(e=0),void 0===t&&(t=this.params.speed),void 0===s&&(s=!0),"string"==typeof e){const t=parseInt(e,10);if(!isFinite(t))throw new Error(`The passed-in 'index' (string) couldn't be converted to 'number'. [${e}] given.`);e=t}const i=this;let r=e;return i.params.loop&&(r+=i.loopedSlides),i.slideTo(r,t,s,a)},slideNext:function(e,t,s){void 0===e&&(e=this.params.speed),void 0===t&&(t=!0);const a=this,{animating:i,enabled:r,params:n}=a;if(!r)return a;let l=n.slidesPerGroup;"auto"===n.slidesPerView&&1===n.slidesPerGroup&&n.slidesPerGroupAuto&&(l=Math.max(a.slidesPerViewDynamic("current",!0),1));const o=a.activeIndexc(e)));let h=n[u.indexOf(p)-1];if(void 0===h&&i.cssMode){let e;n.forEach(((t,s)=>{p>=t&&(e=s)})),void 0!==e&&(h=n[e>0?e-1:e])}let m=0;if(void 0!==h&&(m=l.indexOf(h),m<0&&(m=a.activeIndex-1),"auto"===i.slidesPerView&&1===i.slidesPerGroup&&i.slidesPerGroupAuto&&(m=m-a.slidesPerViewDynamic("previous",!0)+1,m=Math.max(m,0))),i.rewind&&a.isBeginning){const i=a.params.virtual&&a.params.virtual.enabled&&a.virtual?a.virtual.slides.length-1:a.slides.length-1;return a.slideTo(i,e,t,s)}return a.slideTo(m,e,t,s)},slideReset:function(e,t,s){return void 0===e&&(e=this.params.speed),void 0===t&&(t=!0),this.slideTo(this.activeIndex,e,t,s)},slideToClosest:function(e,t,s,a){void 0===e&&(e=this.params.speed),void 0===t&&(t=!0),void 0===a&&(a=.5);const i=this;let r=i.activeIndex;const n=Math.min(i.params.slidesPerGroupSkip,r),l=n+Math.floor((r-n)/i.params.slidesPerGroup),o=i.rtlTranslate?i.translate:-i.translate;if(o>=i.snapGrid[l]){const e=i.snapGrid[l];o-e>(i.snapGrid[l+1]-e)*a&&(r+=i.params.slidesPerGroup)}else{const e=i.snapGrid[l-1];o-e<=(i.snapGrid[l]-e)*a&&(r-=i.params.slidesPerGroup)}return r=Math.max(r,0),r=Math.min(r,i.slidesGrid.length-1),i.slideTo(r,e,t,s)},slideToClickedSlide:function(){const e=this,{params:t,$wrapperEl:s}=e,a="auto"===t.slidesPerView?e.slidesPerViewDynamic():t.slidesPerView;let i,r=e.clickedIndex;if(t.loop){if(e.animating)return;i=parseInt(d(e.clickedSlide).attr("data-swiper-slide-index"),10),t.centeredSlides?re.slides.length-e.loopedSlides+a/2?(e.loopFix(),r=s.children(`.${t.slideClass}[data-swiper-slide-index="${i}"]:not(.${t.slideDuplicateClass})`).eq(0).index(),p((()=>{e.slideTo(r)}))):e.slideTo(r):r>e.slides.length-a?(e.loopFix(),r=s.children(`.${t.slideClass}[data-swiper-slide-index="${i}"]:not(.${t.slideDuplicateClass})`).eq(0).index(),p((()=>{e.slideTo(r)}))):e.slideTo(r)}else e.slideTo(r)}};var z={loopCreate:function(){const e=this,t=a(),{params:s,$wrapperEl:i}=e,r=i.children().length>0?d(i.children()[0].parentNode):i;r.children(`.${s.slideClass}.${s.slideDuplicateClass}`).remove();let n=r.children(`.${s.slideClass}`);if(s.loopFillGroupWithBlank){const e=s.slidesPerGroup-n.length%s.slidesPerGroup;if(e!==s.slidesPerGroup){for(let a=0;an.length&&e.params.loopedSlidesLimit&&(e.loopedSlides=n.length);const l=[],o=[];n.each(((e,t)=>{d(e).attr("data-swiper-slide-index",t)}));for(let t=0;t=0;e-=1)r.prepend(d(l[e].cloneNode(!0)).addClass(s.slideDuplicateClass))},loopFix:function(){const e=this;e.emit("beforeLoopFix");const{activeIndex:t,slides:s,loopedSlides:a,allowSlidePrev:i,allowSlideNext:r,snapGrid:n,rtlTranslate:l}=e;let o;e.allowSlidePrev=!0,e.allowSlideNext=!0;const d=-n[t]-e.getTranslate();if(t=s.length-a){o=-s.length+t+a,o+=a;e.slideTo(o,0,!1,!0)&&0!==d&&e.setTranslate((l?-e.translate:e.translate)-d)}e.allowSlidePrev=i,e.allowSlideNext=r,e.emit("loopFix")},loopDestroy:function(){const{$wrapperEl:e,params:t,slides:s}=this;e.children(`.${t.slideClass}.${t.slideDuplicateClass},.${t.slideClass}.${t.slideBlankClass}`).remove(),s.removeAttr("data-swiper-slide-index")}};function L(e){const t=this,s=a(),i=r(),n=t.touchEventsData,{params:l,touches:o,enabled:c}=t;if(!c)return;if(t.animating&&l.preventInteractionOnTransition)return;!t.animating&&l.cssMode&&l.loop&&t.loopFix();let p=e;p.originalEvent&&(p=p.originalEvent);let h=d(p.target);if("wrapper"===l.touchEventsTarget&&!h.closest(t.wrapperEl).length)return;if(n.isTouchEvent="touchstart"===p.type,!n.isTouchEvent&&"which"in p&&3===p.which)return;if(!n.isTouchEvent&&"button"in p&&p.button>0)return;if(n.isTouched&&n.isMoved)return;const m=!!l.noSwipingClass&&""!==l.noSwipingClass,f=e.composedPath?e.composedPath():e.path;m&&p.target&&p.target.shadowRoot&&f&&(h=d(f[0]));const g=l.noSwipingSelector?l.noSwipingSelector:`.${l.noSwipingClass}`,v=!(!p.target||!p.target.shadowRoot);if(l.noSwiping&&(v?function(e,t){return void 0===t&&(t=this),function t(s){if(!s||s===a()||s===r())return null;s.assignedSlot&&(s=s.assignedSlot);const i=s.closest(e);return i||s.getRootNode?i||t(s.getRootNode().host):null}(t)}(g,h[0]):h.closest(g)[0]))return void(t.allowClick=!0);if(l.swipeHandler&&!h.closest(l.swipeHandler)[0])return;o.currentX="touchstart"===p.type?p.targetTouches[0].pageX:p.pageX,o.currentY="touchstart"===p.type?p.targetTouches[0].pageY:p.pageY;const w=o.currentX,b=o.currentY,x=l.edgeSwipeDetection||l.iOSEdgeSwipeDetection,y=l.edgeSwipeThreshold||l.iOSEdgeSwipeThreshold;if(x&&(w<=y||w>=i.innerWidth-y)){if("prevent"!==x)return;e.preventDefault()}if(Object.assign(n,{isTouched:!0,isMoved:!1,allowTouchCallbacks:!0,isScrolling:void 0,startMoving:void 0}),o.startX=w,o.startY=b,n.touchStartTime=u(),t.allowClick=!0,t.updateSize(),t.swipeDirection=void 0,l.threshold>0&&(n.allowThresholdMove=!1),"touchstart"!==p.type){let e=!0;h.is(n.focusableElements)&&(e=!1,"SELECT"===h[0].nodeName&&(n.isTouched=!1)),s.activeElement&&d(s.activeElement).is(n.focusableElements)&&s.activeElement!==h[0]&&s.activeElement.blur();const a=e&&t.allowTouchMove&&l.touchStartPreventDefault;!l.touchStartForcePreventDefault&&!a||h[0].isContentEditable||p.preventDefault()}t.params.freeMode&&t.params.freeMode.enabled&&t.freeMode&&t.animating&&!l.cssMode&&t.freeMode.onTouchStart(),t.emit("touchStart",p)}function O(e){const t=a(),s=this,i=s.touchEventsData,{params:r,touches:n,rtlTranslate:l,enabled:o}=s;if(!o)return;let c=e;if(c.originalEvent&&(c=c.originalEvent),!i.isTouched)return void(i.startMoving&&i.isScrolling&&s.emit("touchMoveOpposite",c));if(i.isTouchEvent&&"touchmove"!==c.type)return;const p="touchmove"===c.type&&c.targetTouches&&(c.targetTouches[0]||c.changedTouches[0]),h="touchmove"===c.type?p.pageX:c.pageX,m="touchmove"===c.type?p.pageY:c.pageY;if(c.preventedByNestedSwiper)return n.startX=h,void(n.startY=m);if(!s.allowTouchMove)return d(c.target).is(i.focusableElements)||(s.allowClick=!1),void(i.isTouched&&(Object.assign(n,{startX:h,startY:m,currentX:h,currentY:m}),i.touchStartTime=u()));if(i.isTouchEvent&&r.touchReleaseOnEdges&&!r.loop)if(s.isVertical()){if(mn.startY&&s.translate>=s.minTranslate())return i.isTouched=!1,void(i.isMoved=!1)}else if(hn.startX&&s.translate>=s.minTranslate())return;if(i.isTouchEvent&&t.activeElement&&c.target===t.activeElement&&d(c.target).is(i.focusableElements))return i.isMoved=!0,void(s.allowClick=!1);if(i.allowTouchCallbacks&&s.emit("touchMove",c),c.targetTouches&&c.targetTouches.length>1)return;n.currentX=h,n.currentY=m;const f=n.currentX-n.startX,g=n.currentY-n.startY;if(s.params.threshold&&Math.sqrt(f**2+g**2)=25&&(e=180*Math.atan2(Math.abs(g),Math.abs(f))/Math.PI,i.isScrolling=s.isHorizontal()?e>r.touchAngle:90-e>r.touchAngle)}if(i.isScrolling&&s.emit("touchMoveOpposite",c),void 0===i.startMoving&&(n.currentX===n.startX&&n.currentY===n.startY||(i.startMoving=!0)),i.isScrolling)return void(i.isTouched=!1);if(!i.startMoving)return;s.allowClick=!1,!r.cssMode&&c.cancelable&&c.preventDefault(),r.touchMoveStopPropagation&&!r.nested&&c.stopPropagation(),i.isMoved||(r.loop&&!r.cssMode&&s.loopFix(),i.startTranslate=s.getTranslate(),s.setTransition(0),s.animating&&s.$wrapperEl.trigger("webkitTransitionEnd transitionend"),i.allowMomentumBounce=!1,!r.grabCursor||!0!==s.allowSlideNext&&!0!==s.allowSlidePrev||s.setGrabCursor(!0),s.emit("sliderFirstMove",c)),s.emit("sliderMove",c),i.isMoved=!0;let v=s.isHorizontal()?f:g;n.diff=v,v*=r.touchRatio,l&&(v=-v),s.swipeDirection=v>0?"prev":"next",i.currentTranslate=v+i.startTranslate;let w=!0,b=r.resistanceRatio;if(r.touchReleaseOnEdges&&(b=0),v>0&&i.currentTranslate>s.minTranslate()?(w=!1,r.resistance&&(i.currentTranslate=s.minTranslate()-1+(-s.minTranslate()+i.startTranslate+v)**b)):v<0&&i.currentTranslatei.startTranslate&&(i.currentTranslate=i.startTranslate),s.allowSlidePrev||s.allowSlideNext||(i.currentTranslate=i.startTranslate),r.threshold>0){if(!(Math.abs(v)>r.threshold||i.allowThresholdMove))return void(i.currentTranslate=i.startTranslate);if(!i.allowThresholdMove)return i.allowThresholdMove=!0,n.startX=n.currentX,n.startY=n.currentY,i.currentTranslate=i.startTranslate,void(n.diff=s.isHorizontal()?n.currentX-n.startX:n.currentY-n.startY)}r.followFinger&&!r.cssMode&&((r.freeMode&&r.freeMode.enabled&&s.freeMode||r.watchSlidesProgress)&&(s.updateActiveIndex(),s.updateSlidesClasses()),s.params.freeMode&&r.freeMode.enabled&&s.freeMode&&s.freeMode.onTouchMove(),s.updateProgress(i.currentTranslate),s.setTranslate(i.currentTranslate))}function I(e){const t=this,s=t.touchEventsData,{params:a,touches:i,rtlTranslate:r,slidesGrid:n,enabled:l}=t;if(!l)return;let o=e;if(o.originalEvent&&(o=o.originalEvent),s.allowTouchCallbacks&&t.emit("touchEnd",o),s.allowTouchCallbacks=!1,!s.isTouched)return s.isMoved&&a.grabCursor&&t.setGrabCursor(!1),s.isMoved=!1,void(s.startMoving=!1);a.grabCursor&&s.isMoved&&s.isTouched&&(!0===t.allowSlideNext||!0===t.allowSlidePrev)&&t.setGrabCursor(!1);const d=u(),c=d-s.touchStartTime;if(t.allowClick){const e=o.path||o.composedPath&&o.composedPath();t.updateClickedSlide(e&&e[0]||o.target),t.emit("tap click",o),c<300&&d-s.lastClickTime<300&&t.emit("doubleTap doubleClick",o)}if(s.lastClickTime=u(),p((()=>{t.destroyed||(t.allowClick=!0)})),!s.isTouched||!s.isMoved||!t.swipeDirection||0===i.diff||s.currentTranslate===s.startTranslate)return s.isTouched=!1,s.isMoved=!1,void(s.startMoving=!1);let h;if(s.isTouched=!1,s.isMoved=!1,s.startMoving=!1,h=a.followFinger?r?t.translate:-t.translate:-s.currentTranslate,a.cssMode)return;if(t.params.freeMode&&a.freeMode.enabled)return void t.freeMode.onTouchEnd({currentPos:h});let m=0,f=t.slidesSizesGrid[0];for(let e=0;e=n[e]&&h=n[e]&&(m=e,f=n[n.length-1]-n[n.length-2])}let g=null,v=null;a.rewind&&(t.isBeginning?v=t.params.virtual&&t.params.virtual.enabled&&t.virtual?t.virtual.slides.length-1:t.slides.length-1:t.isEnd&&(g=0));const w=(h-n[m])/f,b=ma.longSwipesMs){if(!a.longSwipes)return void t.slideTo(t.activeIndex);"next"===t.swipeDirection&&(w>=a.longSwipesRatio?t.slideTo(a.rewind&&t.isEnd?g:m+b):t.slideTo(m)),"prev"===t.swipeDirection&&(w>1-a.longSwipesRatio?t.slideTo(m+b):null!==v&&w<0&&Math.abs(w)>a.longSwipesRatio?t.slideTo(v):t.slideTo(m))}else{if(!a.shortSwipes)return void t.slideTo(t.activeIndex);t.navigation&&(o.target===t.navigation.nextEl||o.target===t.navigation.prevEl)?o.target===t.navigation.nextEl?t.slideTo(m+b):t.slideTo(m):("next"===t.swipeDirection&&t.slideTo(null!==g?g:m+b),"prev"===t.swipeDirection&&t.slideTo(null!==v?v:m))}}function A(){const e=this,{params:t,el:s}=e;if(s&&0===s.offsetWidth)return;t.breakpoints&&e.setBreakpoint();const{allowSlideNext:a,allowSlidePrev:i,snapGrid:r}=e;e.allowSlideNext=!0,e.allowSlidePrev=!0,e.updateSize(),e.updateSlides(),e.updateSlidesClasses(),("auto"===t.slidesPerView||t.slidesPerView>1)&&e.isEnd&&!e.isBeginning&&!e.params.centeredSlides?e.slideTo(e.slides.length-1,0,!1,!0):e.slideTo(e.activeIndex,0,!1,!0),e.autoplay&&e.autoplay.running&&e.autoplay.paused&&e.autoplay.run(),e.allowSlidePrev=i,e.allowSlideNext=a,e.params.watchOverflow&&r!==e.snapGrid&&e.checkOverflow()}function D(e){const t=this;t.enabled&&(t.allowClick||(t.params.preventClicks&&e.preventDefault(),t.params.preventClicksPropagation&&t.animating&&(e.stopPropagation(),e.stopImmediatePropagation())))}function G(){const e=this,{wrapperEl:t,rtlTranslate:s,enabled:a}=e;if(!a)return;let i;e.previousTranslate=e.translate,e.isHorizontal()?e.translate=-t.scrollLeft:e.translate=-t.scrollTop,0===e.translate&&(e.translate=0),e.updateActiveIndex(),e.updateSlidesClasses();const r=e.maxTranslate()-e.minTranslate();i=0===r?0:(e.translate-e.minTranslate())/r,i!==e.progress&&e.updateProgress(s?-e.translate:e.translate),e.emit("setTranslate",e.translate,!1)}let N=!1;function B(){}const H=(e,t)=>{const s=a(),{params:i,touchEvents:r,el:n,wrapperEl:l,device:o,support:d}=e,c=!!i.nested,p="on"===t?"addEventListener":"removeEventListener",u=t;if(d.touch){const t=!("touchstart"!==r.start||!d.passiveListener||!i.passiveListeners)&&{passive:!0,capture:!1};n[p](r.start,e.onTouchStart,t),n[p](r.move,e.onTouchMove,d.passiveListener?{passive:!1,capture:c}:c),n[p](r.end,e.onTouchEnd,t),r.cancel&&n[p](r.cancel,e.onTouchEnd,t)}else n[p](r.start,e.onTouchStart,!1),s[p](r.move,e.onTouchMove,c),s[p](r.end,e.onTouchEnd,!1);(i.preventClicks||i.preventClicksPropagation)&&n[p]("click",e.onClick,!0),i.cssMode&&l[p]("scroll",e.onScroll),i.updateOnWindowResize?e[u](o.ios||o.android?"resize orientationchange observerUpdate":"resize observerUpdate",A,!0):e[u]("observerUpdate",A,!0)};var X={attachEvents:function(){const e=this,t=a(),{params:s,support:i}=e;e.onTouchStart=L.bind(e),e.onTouchMove=O.bind(e),e.onTouchEnd=I.bind(e),s.cssMode&&(e.onScroll=G.bind(e)),e.onClick=D.bind(e),i.touch&&!N&&(t.addEventListener("touchstart",B),N=!0),H(e,"on")},detachEvents:function(){H(this,"off")}};const Y=(e,t)=>e.grid&&t.grid&&t.grid.rows>1;var R={addClasses:function(){const e=this,{classNames:t,params:s,rtl:a,$el:i,device:r,support:n}=e,l=function(e,t){const s=[];return e.forEach((e=>{"object"==typeof e?Object.keys(e).forEach((a=>{e[a]&&s.push(t+a)})):"string"==typeof e&&s.push(t+e)})),s}(["initialized",s.direction,{"pointer-events":!n.touch},{"free-mode":e.params.freeMode&&s.freeMode.enabled},{autoheight:s.autoHeight},{rtl:a},{grid:s.grid&&s.grid.rows>1},{"grid-column":s.grid&&s.grid.rows>1&&"column"===s.grid.fill},{android:r.android},{ios:r.ios},{"css-mode":s.cssMode},{centered:s.cssMode&&s.centeredSlides},{"watch-progress":s.watchSlidesProgress}],s.containerModifierClass);t.push(...l),i.addClass([...t].join(" ")),e.emitContainerClasses()},removeClasses:function(){const{$el:e,classNames:t}=this;e.removeClass(t.join(" ")),this.emitContainerClasses()}};var W={init:!0,direction:"horizontal",touchEventsTarget:"wrapper",initialSlide:0,speed:300,cssMode:!1,updateOnWindowResize:!0,resizeObserver:!0,nested:!1,createElements:!1,enabled:!0,focusableElements:"input, select, option, textarea, button, video, label",width:null,height:null,preventInteractionOnTransition:!1,userAgent:null,url:null,edgeSwipeDetection:!1,edgeSwipeThreshold:20,autoHeight:!1,setWrapperSize:!1,virtualTranslate:!1,effect:"slide",breakpoints:void 0,breakpointsBase:"window",spaceBetween:0,slidesPerView:1,slidesPerGroup:1,slidesPerGroupSkip:0,slidesPerGroupAuto:!1,centeredSlides:!1,centeredSlidesBounds:!1,slidesOffsetBefore:0,slidesOffsetAfter:0,normalizeSlideIndex:!0,centerInsufficientSlides:!1,watchOverflow:!0,roundLengths:!1,touchRatio:1,touchAngle:45,simulateTouch:!0,shortSwipes:!0,longSwipes:!0,longSwipesRatio:.5,longSwipesMs:300,followFinger:!0,allowTouchMove:!0,threshold:0,touchMoveStopPropagation:!1,touchStartPreventDefault:!0,touchStartForcePreventDefault:!1,touchReleaseOnEdges:!1,uniqueNavElements:!0,resistance:!0,resistanceRatio:.85,watchSlidesProgress:!1,grabCursor:!1,preventClicks:!0,preventClicksPropagation:!0,slideToClickedSlide:!1,preloadImages:!0,updateOnImagesReady:!0,loop:!1,loopAdditionalSlides:0,loopedSlides:null,loopedSlidesLimit:!0,loopFillGroupWithBlank:!1,loopPreventsSlide:!0,rewind:!1,allowSlidePrev:!0,allowSlideNext:!0,swipeHandler:null,noSwiping:!0,noSwipingClass:"swiper-no-swiping",noSwipingSelector:null,passiveListeners:!0,maxBackfaceHiddenSlides:10,containerModifierClass:"swiper-",slideClass:"swiper-slide",slideBlankClass:"swiper-slide-invisible-blank",slideActiveClass:"swiper-slide-active",slideDuplicateActiveClass:"swiper-slide-duplicate-active",slideVisibleClass:"swiper-slide-visible",slideDuplicateClass:"swiper-slide-duplicate",slideNextClass:"swiper-slide-next",slideDuplicateNextClass:"swiper-slide-duplicate-next",slidePrevClass:"swiper-slide-prev",slideDuplicatePrevClass:"swiper-slide-duplicate-prev",wrapperClass:"swiper-wrapper",runCallbacksOnInit:!0,_emitClasses:!1};function q(e,t){return function(s){void 0===s&&(s={});const a=Object.keys(s)[0],i=s[a];"object"==typeof i&&null!==i?(["navigation","pagination","scrollbar"].indexOf(a)>=0&&!0===e[a]&&(e[a]={auto:!0}),a in e&&"enabled"in i?(!0===e[a]&&(e[a]={enabled:!0}),"object"!=typeof e[a]||"enabled"in e[a]||(e[a].enabled=!0),e[a]||(e[a]={enabled:!1}),g(t,s)):g(t,s)):g(t,s)}}const j={eventsEmitter:$,update:S,translate:M,transition:{setTransition:function(e,t){const s=this;s.params.cssMode||s.$wrapperEl.transition(e),s.emit("setTransition",e,t)},transitionStart:function(e,t){void 0===e&&(e=!0);const s=this,{params:a}=s;a.cssMode||(a.autoHeight&&s.updateAutoHeight(),P({swiper:s,runCallbacks:e,direction:t,step:"Start"}))},transitionEnd:function(e,t){void 0===e&&(e=!0);const s=this,{params:a}=s;s.animating=!1,a.cssMode||(s.setTransition(0),P({swiper:s,runCallbacks:e,direction:t,step:"End"}))}},slide:k,loop:z,grabCursor:{setGrabCursor:function(e){const t=this;if(t.support.touch||!t.params.simulateTouch||t.params.watchOverflow&&t.isLocked||t.params.cssMode)return;const s="container"===t.params.touchEventsTarget?t.el:t.wrapperEl;s.style.cursor="move",s.style.cursor=e?"grabbing":"grab"},unsetGrabCursor:function(){const e=this;e.support.touch||e.params.watchOverflow&&e.isLocked||e.params.cssMode||(e["container"===e.params.touchEventsTarget?"el":"wrapperEl"].style.cursor="")}},events:X,breakpoints:{setBreakpoint:function(){const e=this,{activeIndex:t,initialized:s,loopedSlides:a=0,params:i,$el:r}=e,n=i.breakpoints;if(!n||n&&0===Object.keys(n).length)return;const l=e.getBreakpoint(n,e.params.breakpointsBase,e.el);if(!l||e.currentBreakpoint===l)return;const o=(l in n?n[l]:void 0)||e.originalParams,d=Y(e,i),c=Y(e,o),p=i.enabled;d&&!c?(r.removeClass(`${i.containerModifierClass}grid ${i.containerModifierClass}grid-column`),e.emitContainerClasses()):!d&&c&&(r.addClass(`${i.containerModifierClass}grid`),(o.grid.fill&&"column"===o.grid.fill||!o.grid.fill&&"column"===i.grid.fill)&&r.addClass(`${i.containerModifierClass}grid-column`),e.emitContainerClasses()),["navigation","pagination","scrollbar"].forEach((t=>{const s=i[t]&&i[t].enabled,a=o[t]&&o[t].enabled;s&&!a&&e[t].disable(),!s&&a&&e[t].enable()}));const u=o.direction&&o.direction!==i.direction,h=i.loop&&(o.slidesPerView!==i.slidesPerView||u);u&&s&&e.changeDirection(),g(e.params,o);const m=e.params.enabled;Object.assign(e,{allowTouchMove:e.params.allowTouchMove,allowSlideNext:e.params.allowSlideNext,allowSlidePrev:e.params.allowSlidePrev}),p&&!m?e.disable():!p&&m&&e.enable(),e.currentBreakpoint=l,e.emit("_beforeBreakpoint",o),h&&s&&(e.loopDestroy(),e.loopCreate(),e.updateSlides(),e.slideTo(t-a+e.loopedSlides,0,!1)),e.emit("breakpoint",o)},getBreakpoint:function(e,t,s){if(void 0===t&&(t="window"),!e||"container"===t&&!s)return;let a=!1;const i=r(),n="window"===t?i.innerHeight:s.clientHeight,l=Object.keys(e).map((e=>{if("string"==typeof e&&0===e.indexOf("@")){const t=parseFloat(e.substr(1));return{value:n*t,point:e}}return{value:e,point:e}}));l.sort(((e,t)=>parseInt(e.value,10)-parseInt(t.value,10)));for(let e=0;es}else e.isLocked=1===e.snapGrid.length;!0===s.allowSlideNext&&(e.allowSlideNext=!e.isLocked),!0===s.allowSlidePrev&&(e.allowSlidePrev=!e.isLocked),t&&t!==e.isLocked&&(e.isEnd=!1),t!==e.isLocked&&e.emit(e.isLocked?"lock":"unlock")}},classes:R,images:{loadImage:function(e,t,s,a,i,n){const l=r();let o;function c(){n&&n()}d(e).parent("picture")[0]||e.complete&&i?c():t?(o=new l.Image,o.onload=c,o.onerror=c,a&&(o.sizes=a),s&&(o.srcset=s),t&&(o.src=t)):c()},preloadImages:function(){const e=this;function t(){null!=e&&e&&!e.destroyed&&(void 0!==e.imagesLoaded&&(e.imagesLoaded+=1),e.imagesLoaded===e.imagesToLoad.length&&(e.params.updateOnImagesReady&&e.update(),e.emit("imagesReady")))}e.imagesToLoad=e.$el.find("img");for(let s=0;s1){const e=[];return d(t.el).each((s=>{const a=g({},t,{el:s});e.push(new V(a))})),e}const r=this;r.__swiper__=!0,r.support=E(),r.device=C({userAgent:t.userAgent}),r.browser=T(),r.eventsListeners={},r.eventsAnyListeners=[],r.modules=[...r.__modules__],t.modules&&Array.isArray(t.modules)&&r.modules.push(...t.modules);const n={};r.modules.forEach((e=>{e({swiper:r,extendParams:q(t,n),on:r.on.bind(r),once:r.once.bind(r),off:r.off.bind(r),emit:r.emit.bind(r)})}));const l=g({},W,n);return r.params=g({},l,_,t),r.originalParams=g({},r.params),r.passedParams=g({},t),r.params&&r.params.on&&Object.keys(r.params.on).forEach((e=>{r.on(e,r.params.on[e])})),r.params&&r.params.onAny&&r.onAny(r.params.onAny),r.$=d,Object.assign(r,{enabled:r.params.enabled,el:e,classNames:[],slides:d(),slidesGrid:[],snapGrid:[],slidesSizesGrid:[],isHorizontal:()=>"horizontal"===r.params.direction,isVertical:()=>"vertical"===r.params.direction,activeIndex:0,realIndex:0,isBeginning:!0,isEnd:!1,translate:0,previousTranslate:0,progress:0,velocity:0,animating:!1,allowSlideNext:r.params.allowSlideNext,allowSlidePrev:r.params.allowSlidePrev,touchEvents:function(){const e=["touchstart","touchmove","touchend","touchcancel"],t=["pointerdown","pointermove","pointerup"];return r.touchEventsTouch={start:e[0],move:e[1],end:e[2],cancel:e[3]},r.touchEventsDesktop={start:t[0],move:t[1],end:t[2]},r.support.touch||!r.params.simulateTouch?r.touchEventsTouch:r.touchEventsDesktop}(),touchEventsData:{isTouched:void 0,isMoved:void 0,allowTouchCallbacks:void 0,touchStartTime:void 0,isScrolling:void 0,currentTranslate:void 0,startTranslate:void 0,allowThresholdMove:void 0,focusableElements:r.params.focusableElements,lastClickTime:u(),clickTimeout:void 0,velocities:[],allowMomentumBounce:void 0,isTouchEvent:void 0,startMoving:void 0},allowClick:!0,allowTouchMove:r.params.allowTouchMove,touches:{startX:0,startY:0,currentX:0,currentY:0,diff:0},imagesToLoad:[],imagesLoaded:0}),r.emit("_swiper"),r.params.init&&r.init(),r}enable(){const e=this;e.enabled||(e.enabled=!0,e.params.grabCursor&&e.setGrabCursor(),e.emit("enable"))}disable(){const e=this;e.enabled&&(e.enabled=!1,e.params.grabCursor&&e.unsetGrabCursor(),e.emit("disable"))}setProgress(e,t){const s=this;e=Math.min(Math.max(e,0),1);const a=s.minTranslate(),i=(s.maxTranslate()-a)*e+a;s.translateTo(i,void 0===t?0:t),s.updateActiveIndex(),s.updateSlidesClasses()}emitContainerClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=e.el.className.split(" ").filter((t=>0===t.indexOf("swiper")||0===t.indexOf(e.params.containerModifierClass)));e.emit("_containerClasses",t.join(" "))}getSlideClasses(e){const t=this;return t.destroyed?"":e.className.split(" ").filter((e=>0===e.indexOf("swiper-slide")||0===e.indexOf(t.params.slideClass))).join(" ")}emitSlidesClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=[];e.slides.each((s=>{const a=e.getSlideClasses(s);t.push({slideEl:s,classNames:a}),e.emit("_slideClass",s,a)})),e.emit("_slideClasses",t)}slidesPerViewDynamic(e,t){void 0===e&&(e="current"),void 0===t&&(t=!1);const{params:s,slides:a,slidesGrid:i,slidesSizesGrid:r,size:n,activeIndex:l}=this;let o=1;if(s.centeredSlides){let e,t=a[l].swiperSlideSize;for(let s=l+1;sn&&(e=!0));for(let s=l-1;s>=0;s-=1)a[s]&&!e&&(t+=a[s].swiperSlideSize,o+=1,t>n&&(e=!0))}else if("current"===e)for(let e=l+1;e=0;e-=1){i[l]-i[e]1)&&e.isEnd&&!e.params.centeredSlides?e.slideTo(e.slides.length-1,0,!1,!0):e.slideTo(e.activeIndex,0,!1,!0),i||a()),s.watchOverflow&&t!==e.snapGrid&&e.checkOverflow(),e.emit("update")}changeDirection(e,t){void 0===t&&(t=!0);const s=this,a=s.params.direction;return e||(e="horizontal"===a?"vertical":"horizontal"),e===a||"horizontal"!==e&&"vertical"!==e||(s.$el.removeClass(`${s.params.containerModifierClass}${a}`).addClass(`${s.params.containerModifierClass}${e}`),s.emitContainerClasses(),s.params.direction=e,s.slides.each((t=>{"vertical"===e?t.style.width="":t.style.height=""})),s.emit("changeDirection"),t&&s.update()),s}changeLanguageDirection(e){const t=this;t.rtl&&"rtl"===e||!t.rtl&&"ltr"===e||(t.rtl="rtl"===e,t.rtlTranslate="horizontal"===t.params.direction&&t.rtl,t.rtl?(t.$el.addClass(`${t.params.containerModifierClass}rtl`),t.el.dir="rtl"):(t.$el.removeClass(`${t.params.containerModifierClass}rtl`),t.el.dir="ltr"),t.update())}mount(e){const t=this;if(t.mounted)return!0;const s=d(e||t.params.el);if(!(e=s[0]))return!1;e.swiper=t;const i=()=>`.${(t.params.wrapperClass||"").trim().split(" ").join(".")}`;let r=(()=>{if(e&&e.shadowRoot&&e.shadowRoot.querySelector){const t=d(e.shadowRoot.querySelector(i()));return t.children=e=>s.children(e),t}return s.children?s.children(i()):d(s).children(i())})();if(0===r.length&&t.params.createElements){const e=a().createElement("div");r=d(e),e.className=t.params.wrapperClass,s.append(e),s.children(`.${t.params.slideClass}`).each((e=>{r.append(e)}))}return Object.assign(t,{$el:s,el:e,$wrapperEl:r,wrapperEl:r[0],mounted:!0,rtl:"rtl"===e.dir.toLowerCase()||"rtl"===s.css("direction"),rtlTranslate:"horizontal"===t.params.direction&&("rtl"===e.dir.toLowerCase()||"rtl"===s.css("direction")),wrongRTL:"-webkit-box"===r.css("display")}),!0}init(e){const t=this;if(t.initialized)return t;return!1===t.mount(e)||(t.emit("beforeInit"),t.params.breakpoints&&t.setBreakpoint(),t.addClasses(),t.params.loop&&t.loopCreate(),t.updateSize(),t.updateSlides(),t.params.watchOverflow&&t.checkOverflow(),t.params.grabCursor&&t.enabled&&t.setGrabCursor(),t.params.preloadImages&&t.preloadImages(),t.params.loop?t.slideTo(t.params.initialSlide+t.loopedSlides,0,t.params.runCallbacksOnInit,!1,!0):t.slideTo(t.params.initialSlide,0,t.params.runCallbacksOnInit,!1,!0),t.attachEvents(),t.initialized=!0,t.emit("init"),t.emit("afterInit")),t}destroy(e,t){void 0===e&&(e=!0),void 0===t&&(t=!0);const s=this,{params:a,$el:i,$wrapperEl:r,slides:n}=s;return void 0===s.params||s.destroyed||(s.emit("beforeDestroy"),s.initialized=!1,s.detachEvents(),a.loop&&s.loopDestroy(),t&&(s.removeClasses(),i.removeAttr("style"),r.removeAttr("style"),n&&n.length&&n.removeClass([a.slideVisibleClass,a.slideActiveClass,a.slideNextClass,a.slidePrevClass].join(" ")).removeAttr("style").removeAttr("data-swiper-slide-index")),s.emit("destroy"),Object.keys(s.eventsListeners).forEach((e=>{s.off(e)})),!1!==e&&(s.$el[0].swiper=null,function(e){const t=e;Object.keys(t).forEach((e=>{try{t[e]=null}catch(e){}try{delete t[e]}catch(e){}}))}(s)),s.destroyed=!0),null}static extendDefaults(e){g(_,e)}static get extendedDefaults(){return _}static get defaults(){return W}static installModule(e){V.prototype.__modules__||(V.prototype.__modules__=[]);const t=V.prototype.__modules__;"function"==typeof e&&t.indexOf(e)<0&&t.push(e)}static use(e){return Array.isArray(e)?(e.forEach((e=>V.installModule(e))),V):(V.installModule(e),V)}}function F(e,t,s,i){const r=a();return e.params.createElements&&Object.keys(i).forEach((a=>{if(!s[a]&&!0===s.auto){let n=e.$el.children(`.${i[a]}`)[0];n||(n=r.createElement("div"),n.className=i[a],e.$el.append(n)),s[a]=n,t[a]=n}})),s}function U(e){return void 0===e&&(e=""),`.${e.trim().replace(/([\.:!\/])/g,"\\$1").replace(/ /g,".")}`}function K(e){const t=this,{$wrapperEl:s,params:a}=t;if(a.loop&&t.loopDestroy(),"object"==typeof e&&"length"in e)for(let t=0;t=l)return void s.appendSlide(t);let o=n>e?n+1:n;const d=[];for(let t=l-1;t>=e;t-=1){const e=s.slides.eq(t);e.remove(),d.unshift(e)}if("object"==typeof t&&"length"in t){for(let e=0;ee?n+t.length:n}else a.append(t);for(let e=0;e{if(s.params.effect!==t)return;s.classNames.push(`${s.params.containerModifierClass}${t}`),l&&l()&&s.classNames.push(`${s.params.containerModifierClass}3d`);const e=n?n():{};Object.assign(s.params,e),Object.assign(s.originalParams,e)})),a("setTranslate",(()=>{s.params.effect===t&&i()})),a("setTransition",((e,a)=>{s.params.effect===t&&r(a)})),a("transitionEnd",(()=>{if(s.params.effect===t&&o){if(!d||!d().slideShadows)return;s.slides.each((e=>{s.$(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").remove()})),o()}})),a("virtualUpdate",(()=>{s.params.effect===t&&(s.slides.length||(c=!0),requestAnimationFrame((()=>{c&&s.slides&&s.slides.length&&(i(),c=!1)})))}))}function se(e,t){return e.transformEl?t.find(e.transformEl).css({"backface-visibility":"hidden","-webkit-backface-visibility":"hidden"}):t}function ae(e){let{swiper:t,duration:s,transformEl:a,allSlides:i}=e;const{slides:r,activeIndex:n,$wrapperEl:l}=t;if(t.params.virtualTranslate&&0!==s){let e,s=!1;e=i?a?r.find(a):r:a?r.eq(n).find(a):r.eq(n),e.transitionEnd((()=>{if(s)return;if(!t||t.destroyed)return;s=!0,t.animating=!1;const e=["webkitTransitionEnd","transitionend"];for(let t=0;t`),i.append(r)),r}Object.keys(j).forEach((e=>{Object.keys(j[e]).forEach((t=>{V.prototype[t]=j[e][t]}))})),V.use([function(e){let{swiper:t,on:s,emit:a}=e;const i=r();let n=null,l=null;const o=()=>{t&&!t.destroyed&&t.initialized&&(a("beforeResize"),a("resize"))},d=()=>{t&&!t.destroyed&&t.initialized&&a("orientationchange")};s("init",(()=>{t.params.resizeObserver&&void 0!==i.ResizeObserver?t&&!t.destroyed&&t.initialized&&(n=new ResizeObserver((e=>{l=i.requestAnimationFrame((()=>{const{width:s,height:a}=t;let i=s,r=a;e.forEach((e=>{let{contentBoxSize:s,contentRect:a,target:n}=e;n&&n!==t.el||(i=a?a.width:(s[0]||s).inlineSize,r=a?a.height:(s[0]||s).blockSize)})),i===s&&r===a||o()}))})),n.observe(t.el)):(i.addEventListener("resize",o),i.addEventListener("orientationchange",d))})),s("destroy",(()=>{l&&i.cancelAnimationFrame(l),n&&n.unobserve&&t.el&&(n.unobserve(t.el),n=null),i.removeEventListener("resize",o),i.removeEventListener("orientationchange",d)}))},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const n=[],l=r(),o=function(e,t){void 0===t&&(t={});const s=new(l.MutationObserver||l.WebkitMutationObserver)((e=>{if(1===e.length)return void i("observerUpdate",e[0]);const t=function(){i("observerUpdate",e[0])};l.requestAnimationFrame?l.requestAnimationFrame(t):l.setTimeout(t,0)}));s.observe(e,{attributes:void 0===t.attributes||t.attributes,childList:void 0===t.childList||t.childList,characterData:void 0===t.characterData||t.characterData}),n.push(s)};s({observer:!1,observeParents:!1,observeSlideChildren:!1}),a("init",(()=>{if(t.params.observer){if(t.params.observeParents){const e=t.$el.parents();for(let t=0;t{n.forEach((e=>{e.disconnect()})),n.splice(0,n.length)}))}]);const re=[function(e){let t,{swiper:s,extendParams:a,on:i,emit:r}=e;function n(e,t){const a=s.params.virtual;if(a.cache&&s.virtual.cache[t])return s.virtual.cache[t];const i=a.renderSlide?d(a.renderSlide.call(s,e,t)):d(`

${e}
`);return i.attr("data-swiper-slide-index")||i.attr("data-swiper-slide-index",t),a.cache&&(s.virtual.cache[t]=i),i}function l(e){const{slidesPerView:t,slidesPerGroup:a,centeredSlides:i}=s.params,{addSlidesBefore:l,addSlidesAfter:o}=s.params.virtual,{from:d,to:c,slides:p,slidesGrid:u,offset:h}=s.virtual;s.params.cssMode||s.updateActiveIndex();const m=s.activeIndex||0;let f,g,v;f=s.rtlTranslate?"right":s.isHorizontal()?"left":"top",i?(g=Math.floor(t/2)+a+o,v=Math.floor(t/2)+a+l):(g=t+(a-1)+o,v=a+l);const w=Math.max((m||0)-v,0),b=Math.min((m||0)+g,p.length-1),x=(s.slidesGrid[w]||0)-(s.slidesGrid[0]||0);function y(){s.updateSlides(),s.updateProgress(),s.updateSlidesClasses(),s.lazy&&s.params.lazy.enabled&&s.lazy.load(),r("virtualUpdate")}if(Object.assign(s.virtual,{from:w,to:b,offset:x,slidesGrid:s.slidesGrid}),d===w&&c===b&&!e)return s.slidesGrid!==u&&x!==h&&s.slides.css(f,`${x}px`),s.updateProgress(),void r("virtualUpdate");if(s.params.virtual.renderExternal)return s.params.virtual.renderExternal.call(s,{offset:x,from:w,to:b,slides:function(){const e=[];for(let t=w;t<=b;t+=1)e.push(p[t]);return e}()}),void(s.params.virtual.renderExternalUpdate?y():r("virtualUpdate"));const E=[],C=[];if(e)s.$wrapperEl.find(`.${s.params.slideClass}`).remove();else for(let e=d;e<=c;e+=1)(eb)&&s.$wrapperEl.find(`.${s.params.slideClass}[data-swiper-slide-index="${e}"]`).remove();for(let t=0;t=w&&t<=b&&(void 0===c||e?C.push(t):(t>c&&C.push(t),t{s.$wrapperEl.append(n(p[e],e))})),E.sort(((e,t)=>t-e)).forEach((e=>{s.$wrapperEl.prepend(n(p[e],e))})),s.$wrapperEl.children(".swiper-slide").css(f,`${x}px`),y()}a({virtual:{enabled:!1,slides:[],cache:!0,renderSlide:null,renderExternal:null,renderExternalUpdate:!0,addSlidesBefore:0,addSlidesAfter:0}}),s.virtual={cache:{},from:void 0,to:void 0,slides:[],offset:0,slidesGrid:[]},i("beforeInit",(()=>{s.params.virtual.enabled&&(s.virtual.slides=s.params.virtual.slides,s.classNames.push(`${s.params.containerModifierClass}virtual`),s.params.watchSlidesProgress=!0,s.originalParams.watchSlidesProgress=!0,s.params.initialSlide||l())})),i("setTranslate",(()=>{s.params.virtual.enabled&&(s.params.cssMode&&!s._immediateVirtual?(clearTimeout(t),t=setTimeout((()=>{l()}),100)):l())})),i("init update resize",(()=>{s.params.virtual.enabled&&s.params.cssMode&&v(s.wrapperEl,"--swiper-virtual-size",`${s.virtualSize}px`)})),Object.assign(s.virtual,{appendSlide:function(e){if("object"==typeof e&&"length"in e)for(let t=0;t{const a=e[s],r=a.attr("data-swiper-slide-index");r&&a.attr("data-swiper-slide-index",parseInt(r,10)+i),t[parseInt(s,10)+i]=a})),s.virtual.cache=t}l(!0),s.slideTo(a,0)},removeSlide:function(e){if(null==e)return;let t=s.activeIndex;if(Array.isArray(e))for(let a=e.length-1;a>=0;a-=1)s.virtual.slides.splice(e[a],1),s.params.virtual.cache&&delete s.virtual.cache[e[a]],e[a]0&&0===t.$el.parents(`.${t.params.slideActiveClass}`).length)return;const a=t.$el,i=a[0].clientWidth,r=a[0].clientHeight,n=o.innerWidth,l=o.innerHeight,d=t.$el.offset();s&&(d.left-=t.$el[0].scrollLeft);const c=[[d.left,d.top],[d.left+i,d.top],[d.left,d.top+r],[d.left+i,d.top+r]];for(let t=0;t=0&&s[0]<=n&&s[1]>=0&&s[1]<=l){if(0===s[0]&&0===s[1])continue;e=!0}}if(!e)return}t.isHorizontal()?((d||c||p||u)&&(a.preventDefault?a.preventDefault():a.returnValue=!1),((c||u)&&!s||(d||p)&&s)&&t.slideNext(),((d||p)&&!s||(c||u)&&s)&&t.slidePrev()):((d||c||h||m)&&(a.preventDefault?a.preventDefault():a.returnValue=!1),(c||m)&&t.slideNext(),(d||h)&&t.slidePrev()),n("keyPress",i)}}function p(){t.keyboard.enabled||(d(l).on("keydown",c),t.keyboard.enabled=!0)}function u(){t.keyboard.enabled&&(d(l).off("keydown",c),t.keyboard.enabled=!1)}t.keyboard={enabled:!1},s({keyboard:{enabled:!1,onlyInViewport:!0,pageUpDown:!0}}),i("init",(()=>{t.params.keyboard.enabled&&p()})),i("destroy",(()=>{t.keyboard.enabled&&u()})),Object.assign(t.keyboard,{enable:p,disable:u})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const n=r();let l;s({mousewheel:{enabled:!1,releaseOnEdges:!1,invert:!1,forceToAxis:!1,sensitivity:1,eventsTarget:"container",thresholdDelta:null,thresholdTime:null}}),t.mousewheel={enabled:!1};let o,c=u();const h=[];function m(){t.enabled&&(t.mouseEntered=!0)}function f(){t.enabled&&(t.mouseEntered=!1)}function g(e){return!(t.params.mousewheel.thresholdDelta&&e.delta=6&&u()-c<60||(e.direction<0?t.isEnd&&!t.params.loop||t.animating||(t.slideNext(),i("scroll",e.raw)):t.isBeginning&&!t.params.loop||t.animating||(t.slidePrev(),i("scroll",e.raw)),c=(new n.Date).getTime(),!1)))}function v(e){let s=e,a=!0;if(!t.enabled)return;const r=t.params.mousewheel;t.params.cssMode&&s.preventDefault();let n=t.$el;if("container"!==t.params.mousewheel.eventsTarget&&(n=d(t.params.mousewheel.eventsTarget)),!t.mouseEntered&&!n[0].contains(s.target)&&!r.releaseOnEdges)return!0;s.originalEvent&&(s=s.originalEvent);let c=0;const m=t.rtlTranslate?-1:1,f=function(e){let t=0,s=0,a=0,i=0;return"detail"in e&&(s=e.detail),"wheelDelta"in e&&(s=-e.wheelDelta/120),"wheelDeltaY"in e&&(s=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=s,s=0),a=10*t,i=10*s,"deltaY"in e&&(i=e.deltaY),"deltaX"in e&&(a=e.deltaX),e.shiftKey&&!a&&(a=i,i=0),(a||i)&&e.deltaMode&&(1===e.deltaMode?(a*=40,i*=40):(a*=800,i*=800)),a&&!t&&(t=a<1?-1:1),i&&!s&&(s=i<1?-1:1),{spinX:t,spinY:s,pixelX:a,pixelY:i}}(s);if(r.forceToAxis)if(t.isHorizontal()){if(!(Math.abs(f.pixelX)>Math.abs(f.pixelY)))return!0;c=-f.pixelX*m}else{if(!(Math.abs(f.pixelY)>Math.abs(f.pixelX)))return!0;c=-f.pixelY}else c=Math.abs(f.pixelX)>Math.abs(f.pixelY)?-f.pixelX*m:-f.pixelY;if(0===c)return!0;r.invert&&(c=-c);let v=t.getTranslate()+c*r.sensitivity;if(v>=t.minTranslate()&&(v=t.minTranslate()),v<=t.maxTranslate()&&(v=t.maxTranslate()),a=!!t.params.loop||!(v===t.minTranslate()||v===t.maxTranslate()),a&&t.params.nested&&s.stopPropagation(),t.params.freeMode&&t.params.freeMode.enabled){const e={time:u(),delta:Math.abs(c),direction:Math.sign(c)},a=o&&e.time=t.minTranslate()&&(n=t.minTranslate()),n<=t.maxTranslate()&&(n=t.maxTranslate()),t.setTransition(0),t.setTranslate(n),t.updateProgress(),t.updateActiveIndex(),t.updateSlidesClasses(),(!d&&t.isBeginning||!u&&t.isEnd)&&t.updateSlidesClasses(),t.params.freeMode.sticky){clearTimeout(l),l=void 0,h.length>=15&&h.shift();const s=h.length?h[h.length-1]:void 0,a=h[0];if(h.push(e),s&&(e.delta>s.delta||e.direction!==s.direction))h.splice(0);else if(h.length>=15&&e.time-a.time<500&&a.delta-e.delta>=1&&e.delta<=6){const s=c>0?.8:.2;o=e,h.splice(0),l=p((()=>{t.slideToClosest(t.params.speed,!0,void 0,s)}),0)}l||(l=p((()=>{o=e,h.splice(0),t.slideToClosest(t.params.speed,!0,void 0,.5)}),500))}if(a||i("scroll",s),t.params.autoplay&&t.params.autoplayDisableOnInteraction&&t.autoplay.stop(),n===t.minTranslate()||n===t.maxTranslate())return!0}}else{const s={time:u(),delta:Math.abs(c),direction:Math.sign(c),raw:e};h.length>=2&&h.shift();const a=h.length?h[h.length-1]:void 0;if(h.push(s),a?(s.direction!==a.direction||s.delta>a.delta||s.time>a.time+150)&&g(s):g(s),function(e){const s=t.params.mousewheel;if(e.direction<0){if(t.isEnd&&!t.params.loop&&s.releaseOnEdges)return!0}else if(t.isBeginning&&!t.params.loop&&s.releaseOnEdges)return!0;return!1}(s))return!0}return s.preventDefault?s.preventDefault():s.returnValue=!1,!1}function w(e){let s=t.$el;"container"!==t.params.mousewheel.eventsTarget&&(s=d(t.params.mousewheel.eventsTarget)),s[e]("mouseenter",m),s[e]("mouseleave",f),s[e]("wheel",v)}function b(){return t.params.cssMode?(t.wrapperEl.removeEventListener("wheel",v),!0):!t.mousewheel.enabled&&(w("on"),t.mousewheel.enabled=!0,!0)}function x(){return t.params.cssMode?(t.wrapperEl.addEventListener(event,v),!0):!!t.mousewheel.enabled&&(w("off"),t.mousewheel.enabled=!1,!0)}a("init",(()=>{!t.params.mousewheel.enabled&&t.params.cssMode&&x(),t.params.mousewheel.enabled&&b()})),a("destroy",(()=>{t.params.cssMode&&b(),t.mousewheel.enabled&&x()})),Object.assign(t.mousewheel,{enable:b,disable:x})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;function r(e){let s;return e&&(s=d(e),t.params.uniqueNavElements&&"string"==typeof e&&s.length>1&&1===t.$el.find(e).length&&(s=t.$el.find(e))),s}function n(e,s){const a=t.params.navigation;e&&e.length>0&&(e[s?"addClass":"removeClass"](a.disabledClass),e[0]&&"BUTTON"===e[0].tagName&&(e[0].disabled=s),t.params.watchOverflow&&t.enabled&&e[t.isLocked?"addClass":"removeClass"](a.lockClass))}function l(){if(t.params.loop)return;const{$nextEl:e,$prevEl:s}=t.navigation;n(s,t.isBeginning&&!t.params.rewind),n(e,t.isEnd&&!t.params.rewind)}function o(e){e.preventDefault(),(!t.isBeginning||t.params.loop||t.params.rewind)&&(t.slidePrev(),i("navigationPrev"))}function c(e){e.preventDefault(),(!t.isEnd||t.params.loop||t.params.rewind)&&(t.slideNext(),i("navigationNext"))}function p(){const e=t.params.navigation;if(t.params.navigation=F(t,t.originalParams.navigation,t.params.navigation,{nextEl:"swiper-button-next",prevEl:"swiper-button-prev"}),!e.nextEl&&!e.prevEl)return;const s=r(e.nextEl),a=r(e.prevEl);s&&s.length>0&&s.on("click",c),a&&a.length>0&&a.on("click",o),Object.assign(t.navigation,{$nextEl:s,nextEl:s&&s[0],$prevEl:a,prevEl:a&&a[0]}),t.enabled||(s&&s.addClass(e.lockClass),a&&a.addClass(e.lockClass))}function u(){const{$nextEl:e,$prevEl:s}=t.navigation;e&&e.length&&(e.off("click",c),e.removeClass(t.params.navigation.disabledClass)),s&&s.length&&(s.off("click",o),s.removeClass(t.params.navigation.disabledClass))}s({navigation:{nextEl:null,prevEl:null,hideOnClick:!1,disabledClass:"swiper-button-disabled",hiddenClass:"swiper-button-hidden",lockClass:"swiper-button-lock",navigationDisabledClass:"swiper-navigation-disabled"}}),t.navigation={nextEl:null,$nextEl:null,prevEl:null,$prevEl:null},a("init",(()=>{!1===t.params.navigation.enabled?h():(p(),l())})),a("toEdge fromEdge lock unlock",(()=>{l()})),a("destroy",(()=>{u()})),a("enable disable",(()=>{const{$nextEl:e,$prevEl:s}=t.navigation;e&&e[t.enabled?"removeClass":"addClass"](t.params.navigation.lockClass),s&&s[t.enabled?"removeClass":"addClass"](t.params.navigation.lockClass)})),a("click",((e,s)=>{const{$nextEl:a,$prevEl:r}=t.navigation,n=s.target;if(t.params.navigation.hideOnClick&&!d(n).is(r)&&!d(n).is(a)){if(t.pagination&&t.params.pagination&&t.params.pagination.clickable&&(t.pagination.el===n||t.pagination.el.contains(n)))return;let e;a?e=a.hasClass(t.params.navigation.hiddenClass):r&&(e=r.hasClass(t.params.navigation.hiddenClass)),i(!0===e?"navigationShow":"navigationHide"),a&&a.toggleClass(t.params.navigation.hiddenClass),r&&r.toggleClass(t.params.navigation.hiddenClass)}}));const h=()=>{t.$el.addClass(t.params.navigation.navigationDisabledClass),u()};Object.assign(t.navigation,{enable:()=>{t.$el.removeClass(t.params.navigation.navigationDisabledClass),p(),l()},disable:h,update:l,init:p,destroy:u})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const r="swiper-pagination";let n;s({pagination:{el:null,bulletElement:"span",clickable:!1,hideOnClick:!1,renderBullet:null,renderProgressbar:null,renderFraction:null,renderCustom:null,progressbarOpposite:!1,type:"bullets",dynamicBullets:!1,dynamicMainBullets:1,formatFractionCurrent:e=>e,formatFractionTotal:e=>e,bulletClass:`${r}-bullet`,bulletActiveClass:`${r}-bullet-active`,modifierClass:`${r}-`,currentClass:`${r}-current`,totalClass:`${r}-total`,hiddenClass:`${r}-hidden`,progressbarFillClass:`${r}-progressbar-fill`,progressbarOppositeClass:`${r}-progressbar-opposite`,clickableClass:`${r}-clickable`,lockClass:`${r}-lock`,horizontalClass:`${r}-horizontal`,verticalClass:`${r}-vertical`,paginationDisabledClass:`${r}-disabled`}}),t.pagination={el:null,$el:null,bullets:[]};let l=0;function o(){return!t.params.pagination.el||!t.pagination.el||!t.pagination.$el||0===t.pagination.$el.length}function c(e,s){const{bulletActiveClass:a}=t.params.pagination;e[s]().addClass(`${a}-${s}`)[s]().addClass(`${a}-${s}-${s}`)}function p(){const e=t.rtl,s=t.params.pagination;if(o())return;const a=t.virtual&&t.params.virtual.enabled?t.virtual.slides.length:t.slides.length,r=t.pagination.$el;let p;const u=t.params.loop?Math.ceil((a-2*t.loopedSlides)/t.params.slidesPerGroup):t.snapGrid.length;if(t.params.loop?(p=Math.ceil((t.activeIndex-t.loopedSlides)/t.params.slidesPerGroup),p>a-1-2*t.loopedSlides&&(p-=a-2*t.loopedSlides),p>u-1&&(p-=u),p<0&&"bullets"!==t.params.paginationType&&(p=u+p)):p=void 0!==t.snapIndex?t.snapIndex:t.activeIndex||0,"bullets"===s.type&&t.pagination.bullets&&t.pagination.bullets.length>0){const a=t.pagination.bullets;let i,o,u;if(s.dynamicBullets&&(n=a.eq(0)[t.isHorizontal()?"outerWidth":"outerHeight"](!0),r.css(t.isHorizontal()?"width":"height",n*(s.dynamicMainBullets+4)+"px"),s.dynamicMainBullets>1&&void 0!==t.previousIndex&&(l+=p-(t.previousIndex-t.loopedSlides||0),l>s.dynamicMainBullets-1?l=s.dynamicMainBullets-1:l<0&&(l=0)),i=Math.max(p-l,0),o=i+(Math.min(a.length,s.dynamicMainBullets)-1),u=(o+i)/2),a.removeClass(["","-next","-next-next","-prev","-prev-prev","-main"].map((e=>`${s.bulletActiveClass}${e}`)).join(" ")),r.length>1)a.each((e=>{const t=d(e),a=t.index();a===p&&t.addClass(s.bulletActiveClass),s.dynamicBullets&&(a>=i&&a<=o&&t.addClass(`${s.bulletActiveClass}-main`),a===i&&c(t,"prev"),a===o&&c(t,"next"))}));else{const e=a.eq(p),r=e.index();if(e.addClass(s.bulletActiveClass),s.dynamicBullets){const e=a.eq(i),n=a.eq(o);for(let e=i;e<=o;e+=1)a.eq(e).addClass(`${s.bulletActiveClass}-main`);if(t.params.loop)if(r>=a.length){for(let e=s.dynamicMainBullets;e>=0;e-=1)a.eq(a.length-e).addClass(`${s.bulletActiveClass}-main`);a.eq(a.length-s.dynamicMainBullets-1).addClass(`${s.bulletActiveClass}-prev`)}else c(e,"prev"),c(n,"next");else c(e,"prev"),c(n,"next")}}if(s.dynamicBullets){const i=Math.min(a.length,s.dynamicMainBullets+4),r=(n*i-n)/2-u*n,l=e?"right":"left";a.css(t.isHorizontal()?l:"top",`${r}px`)}}if("fraction"===s.type&&(r.find(U(s.currentClass)).text(s.formatFractionCurrent(p+1)),r.find(U(s.totalClass)).text(s.formatFractionTotal(u))),"progressbar"===s.type){let e;e=s.progressbarOpposite?t.isHorizontal()?"vertical":"horizontal":t.isHorizontal()?"horizontal":"vertical";const a=(p+1)/u;let i=1,n=1;"horizontal"===e?i=a:n=a,r.find(U(s.progressbarFillClass)).transform(`translate3d(0,0,0) scaleX(${i}) scaleY(${n})`).transition(t.params.speed)}"custom"===s.type&&s.renderCustom?(r.html(s.renderCustom(t,p+1,u)),i("paginationRender",r[0])):i("paginationUpdate",r[0]),t.params.watchOverflow&&t.enabled&&r[t.isLocked?"addClass":"removeClass"](s.lockClass)}function u(){const e=t.params.pagination;if(o())return;const s=t.virtual&&t.params.virtual.enabled?t.virtual.slides.length:t.slides.length,a=t.pagination.$el;let r="";if("bullets"===e.type){let i=t.params.loop?Math.ceil((s-2*t.loopedSlides)/t.params.slidesPerGroup):t.snapGrid.length;t.params.freeMode&&t.params.freeMode.enabled&&!t.params.loop&&i>s&&(i=s);for(let s=0;s`;a.html(r),t.pagination.bullets=a.find(U(e.bulletClass))}"fraction"===e.type&&(r=e.renderFraction?e.renderFraction.call(t,e.currentClass,e.totalClass):` / `,a.html(r)),"progressbar"===e.type&&(r=e.renderProgressbar?e.renderProgressbar.call(t,e.progressbarFillClass):``,a.html(r)),"custom"!==e.type&&i("paginationRender",t.pagination.$el[0])}function h(){t.params.pagination=F(t,t.originalParams.pagination,t.params.pagination,{el:"swiper-pagination"});const e=t.params.pagination;if(!e.el)return;let s=d(e.el);0!==s.length&&(t.params.uniqueNavElements&&"string"==typeof e.el&&s.length>1&&(s=t.$el.find(e.el),s.length>1&&(s=s.filter((e=>d(e).parents(".swiper")[0]===t.el)))),"bullets"===e.type&&e.clickable&&s.addClass(e.clickableClass),s.addClass(e.modifierClass+e.type),s.addClass(t.isHorizontal()?e.horizontalClass:e.verticalClass),"bullets"===e.type&&e.dynamicBullets&&(s.addClass(`${e.modifierClass}${e.type}-dynamic`),l=0,e.dynamicMainBullets<1&&(e.dynamicMainBullets=1)),"progressbar"===e.type&&e.progressbarOpposite&&s.addClass(e.progressbarOppositeClass),e.clickable&&s.on("click",U(e.bulletClass),(function(e){e.preventDefault();let s=d(this).index()*t.params.slidesPerGroup;t.params.loop&&(s+=t.loopedSlides),t.slideTo(s)})),Object.assign(t.pagination,{$el:s,el:s[0]}),t.enabled||s.addClass(e.lockClass))}function m(){const e=t.params.pagination;if(o())return;const s=t.pagination.$el;s.removeClass(e.hiddenClass),s.removeClass(e.modifierClass+e.type),s.removeClass(t.isHorizontal()?e.horizontalClass:e.verticalClass),t.pagination.bullets&&t.pagination.bullets.removeClass&&t.pagination.bullets.removeClass(e.bulletActiveClass),e.clickable&&s.off("click",U(e.bulletClass))}a("init",(()=>{!1===t.params.pagination.enabled?f():(h(),u(),p())})),a("activeIndexChange",(()=>{(t.params.loop||void 0===t.snapIndex)&&p()})),a("snapIndexChange",(()=>{t.params.loop||p()})),a("slidesLengthChange",(()=>{t.params.loop&&(u(),p())})),a("snapGridLengthChange",(()=>{t.params.loop||(u(),p())})),a("destroy",(()=>{m()})),a("enable disable",(()=>{const{$el:e}=t.pagination;e&&e[t.enabled?"removeClass":"addClass"](t.params.pagination.lockClass)})),a("lock unlock",(()=>{p()})),a("click",((e,s)=>{const a=s.target,{$el:r}=t.pagination;if(t.params.pagination.el&&t.params.pagination.hideOnClick&&r&&r.length>0&&!d(a).hasClass(t.params.pagination.bulletClass)){if(t.navigation&&(t.navigation.nextEl&&a===t.navigation.nextEl||t.navigation.prevEl&&a===t.navigation.prevEl))return;const e=r.hasClass(t.params.pagination.hiddenClass);i(!0===e?"paginationShow":"paginationHide"),r.toggleClass(t.params.pagination.hiddenClass)}}));const f=()=>{t.$el.addClass(t.params.pagination.paginationDisabledClass),t.pagination.$el&&t.pagination.$el.addClass(t.params.pagination.paginationDisabledClass),m()};Object.assign(t.pagination,{enable:()=>{t.$el.removeClass(t.params.pagination.paginationDisabledClass),t.pagination.$el&&t.pagination.$el.removeClass(t.params.pagination.paginationDisabledClass),h(),u(),p()},disable:f,render:u,update:p,init:h,destroy:m})},function(e){let{swiper:t,extendParams:s,on:i,emit:r}=e;const n=a();let l,o,c,u,h=!1,m=null,f=null;function g(){if(!t.params.scrollbar.el||!t.scrollbar.el)return;const{scrollbar:e,rtlTranslate:s,progress:a}=t,{$dragEl:i,$el:r}=e,n=t.params.scrollbar;let l=o,d=(c-o)*a;s?(d=-d,d>0?(l=o-d,d=0):-d+o>c&&(l=c+d)):d<0?(l=o+d,d=0):d+o>c&&(l=c-d),t.isHorizontal()?(i.transform(`translate3d(${d}px, 0, 0)`),i[0].style.width=`${l}px`):(i.transform(`translate3d(0px, ${d}px, 0)`),i[0].style.height=`${l}px`),n.hide&&(clearTimeout(m),r[0].style.opacity=1,m=setTimeout((()=>{r[0].style.opacity=0,r.transition(400)}),1e3))}function v(){if(!t.params.scrollbar.el||!t.scrollbar.el)return;const{scrollbar:e}=t,{$dragEl:s,$el:a}=e;s[0].style.width="",s[0].style.height="",c=t.isHorizontal()?a[0].offsetWidth:a[0].offsetHeight,u=t.size/(t.virtualSize+t.params.slidesOffsetBefore-(t.params.centeredSlides?t.snapGrid[0]:0)),o="auto"===t.params.scrollbar.dragSize?c*u:parseInt(t.params.scrollbar.dragSize,10),t.isHorizontal()?s[0].style.width=`${o}px`:s[0].style.height=`${o}px`,a[0].style.display=u>=1?"none":"",t.params.scrollbar.hide&&(a[0].style.opacity=0),t.params.watchOverflow&&t.enabled&&e.$el[t.isLocked?"addClass":"removeClass"](t.params.scrollbar.lockClass)}function w(e){return t.isHorizontal()?"touchstart"===e.type||"touchmove"===e.type?e.targetTouches[0].clientX:e.clientX:"touchstart"===e.type||"touchmove"===e.type?e.targetTouches[0].clientY:e.clientY}function b(e){const{scrollbar:s,rtlTranslate:a}=t,{$el:i}=s;let r;r=(w(e)-i.offset()[t.isHorizontal()?"left":"top"]-(null!==l?l:o/2))/(c-o),r=Math.max(Math.min(r,1),0),a&&(r=1-r);const n=t.minTranslate()+(t.maxTranslate()-t.minTranslate())*r;t.updateProgress(n),t.setTranslate(n),t.updateActiveIndex(),t.updateSlidesClasses()}function x(e){const s=t.params.scrollbar,{scrollbar:a,$wrapperEl:i}=t,{$el:n,$dragEl:o}=a;h=!0,l=e.target===o[0]||e.target===o?w(e)-e.target.getBoundingClientRect()[t.isHorizontal()?"left":"top"]:null,e.preventDefault(),e.stopPropagation(),i.transition(100),o.transition(100),b(e),clearTimeout(f),n.transition(0),s.hide&&n.css("opacity",1),t.params.cssMode&&t.$wrapperEl.css("scroll-snap-type","none"),r("scrollbarDragStart",e)}function y(e){const{scrollbar:s,$wrapperEl:a}=t,{$el:i,$dragEl:n}=s;h&&(e.preventDefault?e.preventDefault():e.returnValue=!1,b(e),a.transition(0),i.transition(0),n.transition(0),r("scrollbarDragMove",e))}function E(e){const s=t.params.scrollbar,{scrollbar:a,$wrapperEl:i}=t,{$el:n}=a;h&&(h=!1,t.params.cssMode&&(t.$wrapperEl.css("scroll-snap-type",""),i.transition("")),s.hide&&(clearTimeout(f),f=p((()=>{n.css("opacity",0),n.transition(400)}),1e3)),r("scrollbarDragEnd",e),s.snapOnRelease&&t.slideToClosest())}function C(e){const{scrollbar:s,touchEventsTouch:a,touchEventsDesktop:i,params:r,support:l}=t,o=s.$el;if(!o)return;const d=o[0],c=!(!l.passiveListener||!r.passiveListeners)&&{passive:!1,capture:!1},p=!(!l.passiveListener||!r.passiveListeners)&&{passive:!0,capture:!1};if(!d)return;const u="on"===e?"addEventListener":"removeEventListener";l.touch?(d[u](a.start,x,c),d[u](a.move,y,c),d[u](a.end,E,p)):(d[u](i.start,x,c),n[u](i.move,y,c),n[u](i.end,E,p))}function T(){const{scrollbar:e,$el:s}=t;t.params.scrollbar=F(t,t.originalParams.scrollbar,t.params.scrollbar,{el:"swiper-scrollbar"});const a=t.params.scrollbar;if(!a.el)return;let i=d(a.el);t.params.uniqueNavElements&&"string"==typeof a.el&&i.length>1&&1===s.find(a.el).length&&(i=s.find(a.el)),i.addClass(t.isHorizontal()?a.horizontalClass:a.verticalClass);let r=i.find(`.${t.params.scrollbar.dragClass}`);0===r.length&&(r=d(`
`),i.append(r)),Object.assign(e,{$el:i,el:i[0],$dragEl:r,dragEl:r[0]}),a.draggable&&t.params.scrollbar.el&&t.scrollbar.el&&C("on"),i&&i[t.enabled?"removeClass":"addClass"](t.params.scrollbar.lockClass)}function $(){const e=t.params.scrollbar,s=t.scrollbar.$el;s&&s.removeClass(t.isHorizontal()?e.horizontalClass:e.verticalClass),t.params.scrollbar.el&&t.scrollbar.el&&C("off")}s({scrollbar:{el:null,dragSize:"auto",hide:!1,draggable:!1,snapOnRelease:!0,lockClass:"swiper-scrollbar-lock",dragClass:"swiper-scrollbar-drag",scrollbarDisabledClass:"swiper-scrollbar-disabled",horizontalClass:"swiper-scrollbar-horizontal",verticalClass:"swiper-scrollbar-vertical"}}),t.scrollbar={el:null,dragEl:null,$el:null,$dragEl:null},i("init",(()=>{!1===t.params.scrollbar.enabled?S():(T(),v(),g())})),i("update resize observerUpdate lock unlock",(()=>{v()})),i("setTranslate",(()=>{g()})),i("setTransition",((e,s)=>{!function(e){t.params.scrollbar.el&&t.scrollbar.el&&t.scrollbar.$dragEl.transition(e)}(s)})),i("enable disable",(()=>{const{$el:e}=t.scrollbar;e&&e[t.enabled?"removeClass":"addClass"](t.params.scrollbar.lockClass)})),i("destroy",(()=>{$()}));const S=()=>{t.$el.addClass(t.params.scrollbar.scrollbarDisabledClass),t.scrollbar.$el&&t.scrollbar.$el.addClass(t.params.scrollbar.scrollbarDisabledClass),$()};Object.assign(t.scrollbar,{enable:()=>{t.$el.removeClass(t.params.scrollbar.scrollbarDisabledClass),t.scrollbar.$el&&t.scrollbar.$el.removeClass(t.params.scrollbar.scrollbarDisabledClass),T(),v(),g()},disable:S,updateSize:v,setTranslate:g,init:T,destroy:$})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({parallax:{enabled:!1}});const i=(e,s)=>{const{rtl:a}=t,i=d(e),r=a?-1:1,n=i.attr("data-swiper-parallax")||"0";let l=i.attr("data-swiper-parallax-x"),o=i.attr("data-swiper-parallax-y");const c=i.attr("data-swiper-parallax-scale"),p=i.attr("data-swiper-parallax-opacity");if(l||o?(l=l||"0",o=o||"0"):t.isHorizontal()?(l=n,o="0"):(o=n,l="0"),l=l.indexOf("%")>=0?parseInt(l,10)*s*r+"%":l*s*r+"px",o=o.indexOf("%")>=0?parseInt(o,10)*s+"%":o*s+"px",null!=p){const e=p-(p-1)*(1-Math.abs(s));i[0].style.opacity=e}if(null==c)i.transform(`translate3d(${l}, ${o}, 0px)`);else{const e=c-(c-1)*(1-Math.abs(s));i.transform(`translate3d(${l}, ${o}, 0px) scale(${e})`)}},r=()=>{const{$el:e,slides:s,progress:a,snapGrid:r}=t;e.children("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((e=>{i(e,a)})),s.each(((e,s)=>{let n=e.progress;t.params.slidesPerGroup>1&&"auto"!==t.params.slidesPerView&&(n+=Math.ceil(s/2)-a*(r.length-1)),n=Math.min(Math.max(n,-1),1),d(e).find("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((e=>{i(e,n)}))}))};a("beforeInit",(()=>{t.params.parallax.enabled&&(t.params.watchSlidesProgress=!0,t.originalParams.watchSlidesProgress=!0)})),a("init",(()=>{t.params.parallax.enabled&&r()})),a("setTranslate",(()=>{t.params.parallax.enabled&&r()})),a("setTransition",((e,s)=>{t.params.parallax.enabled&&function(e){void 0===e&&(e=t.params.speed);const{$el:s}=t;s.find("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((t=>{const s=d(t);let a=parseInt(s.attr("data-swiper-parallax-duration"),10)||e;0===e&&(a=0),s.transition(a)}))}(s)}))},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const n=r();s({zoom:{enabled:!1,maxRatio:3,minRatio:1,toggle:!0,containerClass:"swiper-zoom-container",zoomedSlideClass:"swiper-slide-zoomed"}}),t.zoom={enabled:!1};let l,o,c,p=1,u=!1;const m={$slideEl:void 0,slideWidth:void 0,slideHeight:void 0,$imageEl:void 0,$imageWrapEl:void 0,maxRatio:3},f={isTouched:void 0,isMoved:void 0,currentX:void 0,currentY:void 0,minX:void 0,minY:void 0,maxX:void 0,maxY:void 0,width:void 0,height:void 0,startX:void 0,startY:void 0,touchesStart:{},touchesCurrent:{}},g={x:void 0,y:void 0,prevPositionX:void 0,prevPositionY:void 0,prevTime:void 0};let v=1;function w(e){if(e.targetTouches.length<2)return 1;const t=e.targetTouches[0].pageX,s=e.targetTouches[0].pageY,a=e.targetTouches[1].pageX,i=e.targetTouches[1].pageY;return Math.sqrt((a-t)**2+(i-s)**2)}function b(e){const s=t.support,a=t.params.zoom;if(o=!1,c=!1,!s.gestures){if("touchstart"!==e.type||"touchstart"===e.type&&e.targetTouches.length<2)return;o=!0,m.scaleStart=w(e)}m.$slideEl&&m.$slideEl.length||(m.$slideEl=d(e.target).closest(`.${t.params.slideClass}`),0===m.$slideEl.length&&(m.$slideEl=t.slides.eq(t.activeIndex)),m.$imageEl=m.$slideEl.find(`.${a.containerClass}`).eq(0).find("picture, img, svg, canvas, .swiper-zoom-target").eq(0),m.$imageWrapEl=m.$imageEl.parent(`.${a.containerClass}`),m.maxRatio=m.$imageWrapEl.attr("data-swiper-zoom")||a.maxRatio,0!==m.$imageWrapEl.length)?(m.$imageEl&&m.$imageEl.transition(0),u=!0):m.$imageEl=void 0}function x(e){const s=t.support,a=t.params.zoom,i=t.zoom;if(!s.gestures){if("touchmove"!==e.type||"touchmove"===e.type&&e.targetTouches.length<2)return;c=!0,m.scaleMove=w(e)}m.$imageEl&&0!==m.$imageEl.length?(s.gestures?i.scale=e.scale*p:i.scale=m.scaleMove/m.scaleStart*p,i.scale>m.maxRatio&&(i.scale=m.maxRatio-1+(i.scale-m.maxRatio+1)**.5),i.scalef.touchesStart.x))return void(f.isTouched=!1);if(!t.isHorizontal()&&(Math.floor(f.minY)===Math.floor(f.startY)&&f.touchesCurrent.yf.touchesStart.y))return void(f.isTouched=!1)}e.cancelable&&e.preventDefault(),e.stopPropagation(),f.isMoved=!0,f.currentX=f.touchesCurrent.x-f.touchesStart.x+f.startX,f.currentY=f.touchesCurrent.y-f.touchesStart.y+f.startY,f.currentXf.maxX&&(f.currentX=f.maxX-1+(f.currentX-f.maxX+1)**.8),f.currentYf.maxY&&(f.currentY=f.maxY-1+(f.currentY-f.maxY+1)**.8),g.prevPositionX||(g.prevPositionX=f.touchesCurrent.x),g.prevPositionY||(g.prevPositionY=f.touchesCurrent.y),g.prevTime||(g.prevTime=Date.now()),g.x=(f.touchesCurrent.x-g.prevPositionX)/(Date.now()-g.prevTime)/2,g.y=(f.touchesCurrent.y-g.prevPositionY)/(Date.now()-g.prevTime)/2,Math.abs(f.touchesCurrent.x-g.prevPositionX)<2&&(g.x=0),Math.abs(f.touchesCurrent.y-g.prevPositionY)<2&&(g.y=0),g.prevPositionX=f.touchesCurrent.x,g.prevPositionY=f.touchesCurrent.y,g.prevTime=Date.now(),m.$imageWrapEl.transform(`translate3d(${f.currentX}px, ${f.currentY}px,0)`)}}function C(){const e=t.zoom;m.$slideEl&&t.previousIndex!==t.activeIndex&&(m.$imageEl&&m.$imageEl.transform("translate3d(0,0,0) scale(1)"),m.$imageWrapEl&&m.$imageWrapEl.transform("translate3d(0,0,0)"),e.scale=1,p=1,m.$slideEl=void 0,m.$imageEl=void 0,m.$imageWrapEl=void 0)}function T(e){const s=t.zoom,a=t.params.zoom;if(m.$slideEl||(e&&e.target&&(m.$slideEl=d(e.target).closest(`.${t.params.slideClass}`)),m.$slideEl||(t.params.virtual&&t.params.virtual.enabled&&t.virtual?m.$slideEl=t.$wrapperEl.children(`.${t.params.slideActiveClass}`):m.$slideEl=t.slides.eq(t.activeIndex)),m.$imageEl=m.$slideEl.find(`.${a.containerClass}`).eq(0).find("picture, img, svg, canvas, .swiper-zoom-target").eq(0),m.$imageWrapEl=m.$imageEl.parent(`.${a.containerClass}`)),!m.$imageEl||0===m.$imageEl.length||!m.$imageWrapEl||0===m.$imageWrapEl.length)return;let i,r,l,o,c,u,h,g,v,w,b,x,y,E,C,T,$,S;t.params.cssMode&&(t.wrapperEl.style.overflow="hidden",t.wrapperEl.style.touchAction="none"),m.$slideEl.addClass(`${a.zoomedSlideClass}`),void 0===f.touchesStart.x&&e?(i="touchend"===e.type?e.changedTouches[0].pageX:e.pageX,r="touchend"===e.type?e.changedTouches[0].pageY:e.pageY):(i=f.touchesStart.x,r=f.touchesStart.y),s.scale=m.$imageWrapEl.attr("data-swiper-zoom")||a.maxRatio,p=m.$imageWrapEl.attr("data-swiper-zoom")||a.maxRatio,e?($=m.$slideEl[0].offsetWidth,S=m.$slideEl[0].offsetHeight,l=m.$slideEl.offset().left+n.scrollX,o=m.$slideEl.offset().top+n.scrollY,c=l+$/2-i,u=o+S/2-r,v=m.$imageEl[0].offsetWidth,w=m.$imageEl[0].offsetHeight,b=v*s.scale,x=w*s.scale,y=Math.min($/2-b/2,0),E=Math.min(S/2-x/2,0),C=-y,T=-E,h=c*s.scale,g=u*s.scale,hC&&(h=C),gT&&(g=T)):(h=0,g=0),m.$imageWrapEl.transition(300).transform(`translate3d(${h}px, ${g}px,0)`),m.$imageEl.transition(300).transform(`translate3d(0,0,0) scale(${s.scale})`)}function $(){const e=t.zoom,s=t.params.zoom;m.$slideEl||(t.params.virtual&&t.params.virtual.enabled&&t.virtual?m.$slideEl=t.$wrapperEl.children(`.${t.params.slideActiveClass}`):m.$slideEl=t.slides.eq(t.activeIndex),m.$imageEl=m.$slideEl.find(`.${s.containerClass}`).eq(0).find("picture, img, svg, canvas, .swiper-zoom-target").eq(0),m.$imageWrapEl=m.$imageEl.parent(`.${s.containerClass}`)),m.$imageEl&&0!==m.$imageEl.length&&m.$imageWrapEl&&0!==m.$imageWrapEl.length&&(t.params.cssMode&&(t.wrapperEl.style.overflow="",t.wrapperEl.style.touchAction=""),e.scale=1,p=1,m.$imageWrapEl.transition(300).transform("translate3d(0,0,0)"),m.$imageEl.transition(300).transform("translate3d(0,0,0) scale(1)"),m.$slideEl.removeClass(`${s.zoomedSlideClass}`),m.$slideEl=void 0)}function S(e){const s=t.zoom;s.scale&&1!==s.scale?$():T(e)}function M(){const e=t.support;return{passiveListener:!("touchstart"!==t.touchEvents.start||!e.passiveListener||!t.params.passiveListeners)&&{passive:!0,capture:!1},activeListenerWithCapture:!e.passiveListener||{passive:!1,capture:!0}}}function P(){return`.${t.params.slideClass}`}function k(e){const{passiveListener:s}=M(),a=P();t.$wrapperEl[e]("gesturestart",a,b,s),t.$wrapperEl[e]("gesturechange",a,x,s),t.$wrapperEl[e]("gestureend",a,y,s)}function z(){l||(l=!0,k("on"))}function L(){l&&(l=!1,k("off"))}function O(){const e=t.zoom;if(e.enabled)return;e.enabled=!0;const s=t.support,{passiveListener:a,activeListenerWithCapture:i}=M(),r=P();s.gestures?(t.$wrapperEl.on(t.touchEvents.start,z,a),t.$wrapperEl.on(t.touchEvents.end,L,a)):"touchstart"===t.touchEvents.start&&(t.$wrapperEl.on(t.touchEvents.start,r,b,a),t.$wrapperEl.on(t.touchEvents.move,r,x,i),t.$wrapperEl.on(t.touchEvents.end,r,y,a),t.touchEvents.cancel&&t.$wrapperEl.on(t.touchEvents.cancel,r,y,a)),t.$wrapperEl.on(t.touchEvents.move,`.${t.params.zoom.containerClass}`,E,i)}function I(){const e=t.zoom;if(!e.enabled)return;const s=t.support;e.enabled=!1;const{passiveListener:a,activeListenerWithCapture:i}=M(),r=P();s.gestures?(t.$wrapperEl.off(t.touchEvents.start,z,a),t.$wrapperEl.off(t.touchEvents.end,L,a)):"touchstart"===t.touchEvents.start&&(t.$wrapperEl.off(t.touchEvents.start,r,b,a),t.$wrapperEl.off(t.touchEvents.move,r,x,i),t.$wrapperEl.off(t.touchEvents.end,r,y,a),t.touchEvents.cancel&&t.$wrapperEl.off(t.touchEvents.cancel,r,y,a)),t.$wrapperEl.off(t.touchEvents.move,`.${t.params.zoom.containerClass}`,E,i)}Object.defineProperty(t.zoom,"scale",{get:()=>v,set(e){if(v!==e){const t=m.$imageEl?m.$imageEl[0]:void 0,s=m.$slideEl?m.$slideEl[0]:void 0;i("zoomChange",e,t,s)}v=e}}),a("init",(()=>{t.params.zoom.enabled&&O()})),a("destroy",(()=>{I()})),a("touchStart",((e,s)=>{t.zoom.enabled&&function(e){const s=t.device;m.$imageEl&&0!==m.$imageEl.length&&(f.isTouched||(s.android&&e.cancelable&&e.preventDefault(),f.isTouched=!0,f.touchesStart.x="touchstart"===e.type?e.targetTouches[0].pageX:e.pageX,f.touchesStart.y="touchstart"===e.type?e.targetTouches[0].pageY:e.pageY))}(s)})),a("touchEnd",((e,s)=>{t.zoom.enabled&&function(){const e=t.zoom;if(!m.$imageEl||0===m.$imageEl.length)return;if(!f.isTouched||!f.isMoved)return f.isTouched=!1,void(f.isMoved=!1);f.isTouched=!1,f.isMoved=!1;let s=300,a=300;const i=g.x*s,r=f.currentX+i,n=g.y*a,l=f.currentY+n;0!==g.x&&(s=Math.abs((r-f.currentX)/g.x)),0!==g.y&&(a=Math.abs((l-f.currentY)/g.y));const o=Math.max(s,a);f.currentX=r,f.currentY=l;const d=f.width*e.scale,c=f.height*e.scale;f.minX=Math.min(m.slideWidth/2-d/2,0),f.maxX=-f.minX,f.minY=Math.min(m.slideHeight/2-c/2,0),f.maxY=-f.minY,f.currentX=Math.max(Math.min(f.currentX,f.maxX),f.minX),f.currentY=Math.max(Math.min(f.currentY,f.maxY),f.minY),m.$imageWrapEl.transition(o).transform(`translate3d(${f.currentX}px, ${f.currentY}px,0)`)}()})),a("doubleTap",((e,s)=>{!t.animating&&t.params.zoom.enabled&&t.zoom.enabled&&t.params.zoom.toggle&&S(s)})),a("transitionEnd",(()=>{t.zoom.enabled&&t.params.zoom.enabled&&C()})),a("slideChange",(()=>{t.zoom.enabled&&t.params.zoom.enabled&&t.params.cssMode&&C()})),Object.assign(t.zoom,{enable:O,disable:I,in:T,out:$,toggle:S})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;s({lazy:{checkInView:!1,enabled:!1,loadPrevNext:!1,loadPrevNextAmount:1,loadOnTransitionStart:!1,scrollingElement:"",elementClass:"swiper-lazy",loadingClass:"swiper-lazy-loading",loadedClass:"swiper-lazy-loaded",preloaderClass:"swiper-lazy-preloader"}}),t.lazy={};let n=!1,l=!1;function o(e,s){void 0===s&&(s=!0);const a=t.params.lazy;if(void 0===e)return;if(0===t.slides.length)return;const r=t.virtual&&t.params.virtual.enabled?t.$wrapperEl.children(`.${t.params.slideClass}[data-swiper-slide-index="${e}"]`):t.slides.eq(e),n=r.find(`.${a.elementClass}:not(.${a.loadedClass}):not(.${a.loadingClass})`);!r.hasClass(a.elementClass)||r.hasClass(a.loadedClass)||r.hasClass(a.loadingClass)||n.push(r[0]),0!==n.length&&n.each((e=>{const n=d(e);n.addClass(a.loadingClass);const l=n.attr("data-background"),c=n.attr("data-src"),p=n.attr("data-srcset"),u=n.attr("data-sizes"),h=n.parent("picture");t.loadImage(n[0],c||l,p,u,!1,(()=>{if(null!=t&&t&&(!t||t.params)&&!t.destroyed){if(l?(n.css("background-image",`url("${l}")`),n.removeAttr("data-background")):(p&&(n.attr("srcset",p),n.removeAttr("data-srcset")),u&&(n.attr("sizes",u),n.removeAttr("data-sizes")),h.length&&h.children("source").each((e=>{const t=d(e);t.attr("data-srcset")&&(t.attr("srcset",t.attr("data-srcset")),t.removeAttr("data-srcset"))})),c&&(n.attr("src",c),n.removeAttr("data-src"))),n.addClass(a.loadedClass).removeClass(a.loadingClass),r.find(`.${a.preloaderClass}`).remove(),t.params.loop&&s){const e=r.attr("data-swiper-slide-index");if(r.hasClass(t.params.slideDuplicateClass)){o(t.$wrapperEl.children(`[data-swiper-slide-index="${e}"]:not(.${t.params.slideDuplicateClass})`).index(),!1)}else{o(t.$wrapperEl.children(`.${t.params.slideDuplicateClass}[data-swiper-slide-index="${e}"]`).index(),!1)}}i("lazyImageReady",r[0],n[0]),t.params.autoHeight&&t.updateAutoHeight()}})),i("lazyImageLoad",r[0],n[0])}))}function c(){const{$wrapperEl:e,params:s,slides:a,activeIndex:i}=t,r=t.virtual&&s.virtual.enabled,n=s.lazy;let c=s.slidesPerView;function p(t){if(r){if(e.children(`.${s.slideClass}[data-swiper-slide-index="${t}"]`).length)return!0}else if(a[t])return!0;return!1}function u(e){return r?d(e).attr("data-swiper-slide-index"):d(e).index()}if("auto"===c&&(c=0),l||(l=!0),t.params.watchSlidesProgress)e.children(`.${s.slideVisibleClass}`).each((e=>{o(r?d(e).attr("data-swiper-slide-index"):d(e).index())}));else if(c>1)for(let e=i;e1||n.loadPrevNextAmount&&n.loadPrevNextAmount>1){const e=n.loadPrevNextAmount,t=Math.ceil(c),s=Math.min(i+t+Math.max(e,t),a.length),r=Math.max(i-Math.max(t,e),0);for(let e=i+t;e0&&o(u(t));const a=e.children(`.${s.slidePrevClass}`);a.length>0&&o(u(a))}}function p(){const e=r();if(!t||t.destroyed)return;const s=t.params.lazy.scrollingElement?d(t.params.lazy.scrollingElement):d(e),a=s[0]===e,i=a?e.innerWidth:s[0].offsetWidth,l=a?e.innerHeight:s[0].offsetHeight,o=t.$el.offset(),{rtlTranslate:u}=t;let h=!1;u&&(o.left-=t.$el[0].scrollLeft);const m=[[o.left,o.top],[o.left+t.width,o.top],[o.left,o.top+t.height],[o.left+t.width,o.top+t.height]];for(let e=0;e=0&&t[0]<=i&&t[1]>=0&&t[1]<=l){if(0===t[0]&&0===t[1])continue;h=!0}}const f=!("touchstart"!==t.touchEvents.start||!t.support.passiveListener||!t.params.passiveListeners)&&{passive:!0,capture:!1};h?(c(),s.off("scroll",p,f)):n||(n=!0,s.on("scroll",p,f))}a("beforeInit",(()=>{t.params.lazy.enabled&&t.params.preloadImages&&(t.params.preloadImages=!1)})),a("init",(()=>{t.params.lazy.enabled&&(t.params.lazy.checkInView?p():c())})),a("scroll",(()=>{t.params.freeMode&&t.params.freeMode.enabled&&!t.params.freeMode.sticky&&c()})),a("scrollbarDragMove resize _freeModeNoMomentumRelease",(()=>{t.params.lazy.enabled&&(t.params.lazy.checkInView?p():c())})),a("transitionStart",(()=>{t.params.lazy.enabled&&(t.params.lazy.loadOnTransitionStart||!t.params.lazy.loadOnTransitionStart&&!l)&&(t.params.lazy.checkInView?p():c())})),a("transitionEnd",(()=>{t.params.lazy.enabled&&!t.params.lazy.loadOnTransitionStart&&(t.params.lazy.checkInView?p():c())})),a("slideChange",(()=>{const{lazy:e,cssMode:s,watchSlidesProgress:a,touchReleaseOnEdges:i,resistanceRatio:r}=t.params;e.enabled&&(s||a&&(i||0===r))&&c()})),a("destroy",(()=>{t.$el&&t.$el.find(`.${t.params.lazy.loadingClass}`).removeClass(t.params.lazy.loadingClass)})),Object.assign(t.lazy,{load:c,loadInSlide:o})},function(e){let{swiper:t,extendParams:s,on:a}=e;function i(e,t){const s=function(){let e,t,s;return(a,i)=>{for(t=-1,e=a.length;e-t>1;)s=e+t>>1,a[s]<=i?t=s:e=s;return e}}();let a,i;return this.x=e,this.y=t,this.lastIndex=e.length-1,this.interpolate=function(e){return e?(i=s(this.x,e),a=i-1,(e-this.x[a])*(this.y[i]-this.y[a])/(this.x[i]-this.x[a])+this.y[a]):0},this}function r(){t.controller.control&&t.controller.spline&&(t.controller.spline=void 0,delete t.controller.spline)}s({controller:{control:void 0,inverse:!1,by:"slide"}}),t.controller={control:void 0},a("beforeInit",(()=>{t.controller.control=t.params.controller.control})),a("update",(()=>{r()})),a("resize",(()=>{r()})),a("observerUpdate",(()=>{r()})),a("setTranslate",((e,s,a)=>{t.controller.control&&t.controller.setTranslate(s,a)})),a("setTransition",((e,s,a)=>{t.controller.control&&t.controller.setTransition(s,a)})),Object.assign(t.controller,{setTranslate:function(e,s){const a=t.controller.control;let r,n;const l=t.constructor;function o(e){const s=t.rtlTranslate?-t.translate:t.translate;"slide"===t.params.controller.by&&(!function(e){t.controller.spline||(t.controller.spline=t.params.loop?new i(t.slidesGrid,e.slidesGrid):new i(t.snapGrid,e.snapGrid))}(e),n=-t.controller.spline.interpolate(-s)),n&&"container"!==t.params.controller.by||(r=(e.maxTranslate()-e.minTranslate())/(t.maxTranslate()-t.minTranslate()),n=(s-t.minTranslate())*r+e.minTranslate()),t.params.controller.inverse&&(n=e.maxTranslate()-n),e.updateProgress(n),e.setTranslate(n,t),e.updateActiveIndex(),e.updateSlidesClasses()}if(Array.isArray(a))for(let e=0;e{s.updateAutoHeight()})),s.$wrapperEl.transitionEnd((()=>{i&&(s.params.loop&&"slide"===t.params.controller.by&&s.loopFix(),s.transitionEnd())})))}if(Array.isArray(i))for(r=0;r{n(e),"BUTTON"!==e[0].tagName&&(o(e,"button"),e.on("keydown",m)),p(e,s),function(e,t){e.attr("aria-controls",t)}(e,t)},w=()=>{t.a11y.clicked=!0},b=()=>{requestAnimationFrame((()=>{requestAnimationFrame((()=>{t.a11y.clicked=!1}))}))},x=e=>{if(t.a11y.clicked)return;const s=e.target.closest(`.${t.params.slideClass}`);if(!s||!t.slides.includes(s))return;const a=t.slides.indexOf(s)===t.activeIndex,i=t.params.watchSlidesProgress&&t.visibleSlides&&t.visibleSlides.includes(s);a||i||(t.isHorizontal()?t.el.scrollLeft=0:t.el.scrollTop=0,t.slideTo(t.slides.indexOf(s),0))},y=()=>{const e=t.params.a11y;e.itemRoleDescriptionMessage&&c(d(t.slides),e.itemRoleDescriptionMessage),e.slideRole&&o(d(t.slides),e.slideRole);const s=t.params.loop?t.slides.filter((e=>!e.classList.contains(t.params.slideDuplicateClass))).length:t.slides.length;e.slideLabelMessage&&t.slides.each(((a,i)=>{const r=d(a),n=t.params.loop?parseInt(r.attr("data-swiper-slide-index"),10):i;p(r,e.slideLabelMessage.replace(/\{\{index\}\}/,n+1).replace(/\{\{slidesLength\}\}/,s))}))},E=()=>{const e=t.params.a11y;t.$el.append(i);const s=t.$el;e.containerRoleDescriptionMessage&&c(s,e.containerRoleDescriptionMessage),e.containerMessage&&p(s,e.containerMessage);const a=t.$wrapperEl,r=e.id||a.attr("id")||`swiper-wrapper-${n=16,void 0===n&&(n=16),"x".repeat(n).replace(/x/g,(()=>Math.round(16*Math.random()).toString(16)))}`;var n;const l=t.params.autoplay&&t.params.autoplay.enabled?"off":"polite";var o;let d,u;o=r,a.attr("id",o),function(e,t){e.attr("aria-live",t)}(a,l),y(),t.navigation&&t.navigation.$nextEl&&(d=t.navigation.$nextEl),t.navigation&&t.navigation.$prevEl&&(u=t.navigation.$prevEl),d&&d.length&&v(d,r,e.nextSlideMessage),u&&u.length&&v(u,r,e.prevSlideMessage),g()&&t.pagination.$el.on("keydown",U(t.params.pagination.bulletClass),m),t.$el.on("focus",x,!0),t.$el.on("pointerdown",w,!0),t.$el.on("pointerup",b,!0)};a("beforeInit",(()=>{i=d(``)})),a("afterInit",(()=>{t.params.a11y.enabled&&E()})),a("slidesLengthChange snapGridLengthChange slidesGridLengthChange",(()=>{t.params.a11y.enabled&&y()})),a("fromEdge toEdge afterInit lock unlock",(()=>{t.params.a11y.enabled&&function(){if(t.params.loop||t.params.rewind||!t.navigation)return;const{$nextEl:e,$prevEl:s}=t.navigation;s&&s.length>0&&(t.isBeginning?(u(s),l(s)):(h(s),n(s))),e&&e.length>0&&(t.isEnd?(u(e),l(e)):(h(e),n(e)))}()})),a("paginationUpdate",(()=>{t.params.a11y.enabled&&function(){const e=t.params.a11y;f()&&t.pagination.bullets.each((s=>{const a=d(s);t.params.pagination.clickable&&(n(a),t.params.pagination.renderBullet||(o(a,"button"),p(a,e.paginationBulletMessage.replace(/\{\{index\}\}/,a.index()+1)))),a.is(`.${t.params.pagination.bulletActiveClass}`)?a.attr("aria-current","true"):a.removeAttr("aria-current")}))}()})),a("destroy",(()=>{t.params.a11y.enabled&&function(){let e,s;i&&i.length>0&&i.remove(),t.navigation&&t.navigation.$nextEl&&(e=t.navigation.$nextEl),t.navigation&&t.navigation.$prevEl&&(s=t.navigation.$prevEl),e&&e.off("keydown",m),s&&s.off("keydown",m),g()&&t.pagination.$el.off("keydown",U(t.params.pagination.bulletClass),m),t.$el.off("focus",x,!0),t.$el.off("pointerdown",w,!0),t.$el.off("pointerup",b,!0)}()}))},function(e){let{swiper:t,extendParams:s,on:a}=e;s({history:{enabled:!1,root:"",replaceState:!1,key:"slides",keepQuery:!1}});let i=!1,n={};const l=e=>e.toString().replace(/\s+/g,"-").replace(/[^\w-]+/g,"").replace(/--+/g,"-").replace(/^-+/,"").replace(/-+$/,""),o=e=>{const t=r();let s;s=e?new URL(e):t.location;const a=s.pathname.slice(1).split("/").filter((e=>""!==e)),i=a.length;return{key:a[i-2],value:a[i-1]}},d=(e,s)=>{const a=r();if(!i||!t.params.history.enabled)return;let n;n=t.params.url?new URL(t.params.url):a.location;const o=t.slides.eq(s);let d=l(o.attr("data-history"));if(t.params.history.root.length>0){let s=t.params.history.root;"/"===s[s.length-1]&&(s=s.slice(0,s.length-1)),d=`${s}/${e}/${d}`}else n.pathname.includes(e)||(d=`${e}/${d}`);t.params.history.keepQuery&&(d+=n.search);const c=a.history.state;c&&c.value===d||(t.params.history.replaceState?a.history.replaceState({value:d},null,d):a.history.pushState({value:d},null,d))},c=(e,s,a)=>{if(s)for(let i=0,r=t.slides.length;i{n=o(t.params.url),c(t.params.speed,n.value,!1)};a("init",(()=>{t.params.history.enabled&&(()=>{const e=r();if(t.params.history){if(!e.history||!e.history.pushState)return t.params.history.enabled=!1,void(t.params.hashNavigation.enabled=!0);i=!0,n=o(t.params.url),(n.key||n.value)&&(c(0,n.value,t.params.runCallbacksOnInit),t.params.history.replaceState||e.addEventListener("popstate",p))}})()})),a("destroy",(()=>{t.params.history.enabled&&(()=>{const e=r();t.params.history.replaceState||e.removeEventListener("popstate",p)})()})),a("transitionEnd _freeModeNoMomentumRelease",(()=>{i&&d(t.params.history.key,t.activeIndex)})),a("slideChange",(()=>{i&&t.params.cssMode&&d(t.params.history.key,t.activeIndex)}))},function(e){let{swiper:t,extendParams:s,emit:i,on:n}=e,l=!1;const o=a(),c=r();s({hashNavigation:{enabled:!1,replaceState:!1,watchState:!1}});const p=()=>{i("hashChange");const e=o.location.hash.replace("#","");if(e!==t.slides.eq(t.activeIndex).attr("data-hash")){const s=t.$wrapperEl.children(`.${t.params.slideClass}[data-hash="${e}"]`).index();if(void 0===s)return;t.slideTo(s)}},u=()=>{if(l&&t.params.hashNavigation.enabled)if(t.params.hashNavigation.replaceState&&c.history&&c.history.replaceState)c.history.replaceState(null,null,`#${t.slides.eq(t.activeIndex).attr("data-hash")}`||""),i("hashSet");else{const e=t.slides.eq(t.activeIndex),s=e.attr("data-hash")||e.attr("data-history");o.location.hash=s||"",i("hashSet")}};n("init",(()=>{t.params.hashNavigation.enabled&&(()=>{if(!t.params.hashNavigation.enabled||t.params.history&&t.params.history.enabled)return;l=!0;const e=o.location.hash.replace("#","");if(e){const s=0;for(let a=0,i=t.slides.length;a{t.params.hashNavigation.enabled&&t.params.hashNavigation.watchState&&d(c).off("hashchange",p)})),n("transitionEnd _freeModeNoMomentumRelease",(()=>{l&&u()})),n("slideChange",(()=>{l&&t.params.cssMode&&u()}))},function(e){let t,{swiper:s,extendParams:i,on:r,emit:n}=e;function l(){if(!s.size)return s.autoplay.running=!1,void(s.autoplay.paused=!1);const e=s.slides.eq(s.activeIndex);let a=s.params.autoplay.delay;e.attr("data-swiper-autoplay")&&(a=e.attr("data-swiper-autoplay")||s.params.autoplay.delay),clearTimeout(t),t=p((()=>{let e;s.params.autoplay.reverseDirection?s.params.loop?(s.loopFix(),e=s.slidePrev(s.params.speed,!0,!0),n("autoplay")):s.isBeginning?s.params.autoplay.stopOnLastSlide?d():(e=s.slideTo(s.slides.length-1,s.params.speed,!0,!0),n("autoplay")):(e=s.slidePrev(s.params.speed,!0,!0),n("autoplay")):s.params.loop?(s.loopFix(),e=s.slideNext(s.params.speed,!0,!0),n("autoplay")):s.isEnd?s.params.autoplay.stopOnLastSlide?d():(e=s.slideTo(0,s.params.speed,!0,!0),n("autoplay")):(e=s.slideNext(s.params.speed,!0,!0),n("autoplay")),(s.params.cssMode&&s.autoplay.running||!1===e)&&l()}),a)}function o(){return void 0===t&&(!s.autoplay.running&&(s.autoplay.running=!0,n("autoplayStart"),l(),!0))}function d(){return!!s.autoplay.running&&(void 0!==t&&(t&&(clearTimeout(t),t=void 0),s.autoplay.running=!1,n("autoplayStop"),!0))}function c(e){s.autoplay.running&&(s.autoplay.paused||(t&&clearTimeout(t),s.autoplay.paused=!0,0!==e&&s.params.autoplay.waitForTransition?["transitionend","webkitTransitionEnd"].forEach((e=>{s.$wrapperEl[0].addEventListener(e,h)})):(s.autoplay.paused=!1,l())))}function u(){const e=a();"hidden"===e.visibilityState&&s.autoplay.running&&c(),"visible"===e.visibilityState&&s.autoplay.paused&&(l(),s.autoplay.paused=!1)}function h(e){s&&!s.destroyed&&s.$wrapperEl&&e.target===s.$wrapperEl[0]&&(["transitionend","webkitTransitionEnd"].forEach((e=>{s.$wrapperEl[0].removeEventListener(e,h)})),s.autoplay.paused=!1,s.autoplay.running?l():d())}function m(){s.params.autoplay.disableOnInteraction?d():(n("autoplayPause"),c()),["transitionend","webkitTransitionEnd"].forEach((e=>{s.$wrapperEl[0].removeEventListener(e,h)}))}function f(){s.params.autoplay.disableOnInteraction||(s.autoplay.paused=!1,n("autoplayResume"),l())}s.autoplay={running:!1,paused:!1},i({autoplay:{enabled:!1,delay:3e3,waitForTransition:!0,disableOnInteraction:!0,stopOnLastSlide:!1,reverseDirection:!1,pauseOnMouseEnter:!1}}),r("init",(()=>{if(s.params.autoplay.enabled){o();a().addEventListener("visibilitychange",u),s.params.autoplay.pauseOnMouseEnter&&(s.$el.on("mouseenter",m),s.$el.on("mouseleave",f))}})),r("beforeTransitionStart",((e,t,a)=>{s.autoplay.running&&(a||!s.params.autoplay.disableOnInteraction?s.autoplay.pause(t):d())})),r("sliderFirstMove",(()=>{s.autoplay.running&&(s.params.autoplay.disableOnInteraction?d():c())})),r("touchEnd",(()=>{s.params.cssMode&&s.autoplay.paused&&!s.params.autoplay.disableOnInteraction&&l()})),r("destroy",(()=>{s.$el.off("mouseenter",m),s.$el.off("mouseleave",f),s.autoplay.running&&d();a().removeEventListener("visibilitychange",u)})),Object.assign(s.autoplay,{pause:c,run:l,start:o,stop:d})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({thumbs:{swiper:null,multipleActiveThumbs:!0,autoScrollOffset:0,slideThumbActiveClass:"swiper-slide-thumb-active",thumbsContainerClass:"swiper-thumbs"}});let i=!1,r=!1;function n(){const e=t.thumbs.swiper;if(!e||e.destroyed)return;const s=e.clickedIndex,a=e.clickedSlide;if(a&&d(a).hasClass(t.params.thumbs.slideThumbActiveClass))return;if(null==s)return;let i;if(i=e.params.loop?parseInt(d(e.clickedSlide).attr("data-swiper-slide-index"),10):s,t.params.loop){let e=t.activeIndex;t.slides.eq(e).hasClass(t.params.slideDuplicateClass)&&(t.loopFix(),t._clientLeft=t.$wrapperEl[0].clientLeft,e=t.activeIndex);const s=t.slides.eq(e).prevAll(`[data-swiper-slide-index="${i}"]`).eq(0).index(),a=t.slides.eq(e).nextAll(`[data-swiper-slide-index="${i}"]`).eq(0).index();i=void 0===s?a:void 0===a?s:a-e1&&!t.params.centeredSlides&&(i=t.params.slidesPerView),t.params.thumbs.multipleActiveThumbs||(i=1),i=Math.floor(i),s.slides.removeClass(r),s.params.loop||s.params.virtual&&s.params.virtual.enabled)for(let e=0;e1?a:o:a-ot.previousIndex?"next":"prev"}else i=t.realIndex,r=i>t.previousIndex?"next":"prev";l&&(i+="next"===r?n:-1*n),s.visibleSlidesIndexes&&s.visibleSlidesIndexes.indexOf(i)<0&&(s.params.centeredSlides?i=i>o?i-Math.floor(a/2)+1:i+Math.floor(a/2)-1:i>o&&s.params.slidesPerGroup,s.slideTo(i,e?0:void 0))}}t.thumbs={swiper:null},a("beforeInit",(()=>{const{thumbs:e}=t.params;e&&e.swiper&&(l(),o(!0))})),a("slideChange update resize observerUpdate",(()=>{o()})),a("setTransition",((e,s)=>{const a=t.thumbs.swiper;a&&!a.destroyed&&a.setTransition(s)})),a("beforeDestroy",(()=>{const e=t.thumbs.swiper;e&&!e.destroyed&&r&&e.destroy()})),Object.assign(t.thumbs,{init:l,update:o})},function(e){let{swiper:t,extendParams:s,emit:a,once:i}=e;s({freeMode:{enabled:!1,momentum:!0,momentumRatio:1,momentumBounce:!0,momentumBounceRatio:1,momentumVelocityRatio:1,sticky:!1,minimumVelocity:.02}}),Object.assign(t,{freeMode:{onTouchStart:function(){const e=t.getTranslate();t.setTranslate(e),t.setTransition(0),t.touchEventsData.velocities.length=0,t.freeMode.onTouchEnd({currentPos:t.rtl?t.translate:-t.translate})},onTouchMove:function(){const{touchEventsData:e,touches:s}=t;0===e.velocities.length&&e.velocities.push({position:s[t.isHorizontal()?"startX":"startY"],time:e.touchStartTime}),e.velocities.push({position:s[t.isHorizontal()?"currentX":"currentY"],time:u()})},onTouchEnd:function(e){let{currentPos:s}=e;const{params:r,$wrapperEl:n,rtlTranslate:l,snapGrid:o,touchEventsData:d}=t,c=u()-d.touchStartTime;if(s<-t.minTranslate())t.slideTo(t.activeIndex);else if(s>-t.maxTranslate())t.slides.length1){const e=d.velocities.pop(),s=d.velocities.pop(),a=e.position-s.position,i=e.time-s.time;t.velocity=a/i,t.velocity/=2,Math.abs(t.velocity)150||u()-e.time>300)&&(t.velocity=0)}else t.velocity=0;t.velocity*=r.freeMode.momentumVelocityRatio,d.velocities.length=0;let e=1e3*r.freeMode.momentumRatio;const s=t.velocity*e;let c=t.translate+s;l&&(c=-c);let p,h=!1;const m=20*Math.abs(t.velocity)*r.freeMode.momentumBounceRatio;let f;if(ct.minTranslate())r.freeMode.momentumBounce?(c-t.minTranslate()>m&&(c=t.minTranslate()+m),p=t.minTranslate(),h=!0,d.allowMomentumBounce=!0):c=t.minTranslate(),r.loop&&r.centeredSlides&&(f=!0);else if(r.freeMode.sticky){let e;for(let t=0;t-c){e=t;break}c=Math.abs(o[e]-c){t.loopFix()})),0!==t.velocity){if(e=l?Math.abs((-c-t.translate)/t.velocity):Math.abs((c-t.translate)/t.velocity),r.freeMode.sticky){const s=Math.abs((l?-c:c)-t.translate),a=t.slidesSizesGrid[t.activeIndex];e=s{t&&!t.destroyed&&d.allowMomentumBounce&&(a("momentumBounce"),t.setTransition(r.speed),setTimeout((()=>{t.setTranslate(p),n.transitionEnd((()=>{t&&!t.destroyed&&t.transitionEnd()}))}),0))}))):t.velocity?(a("_freeModeNoMomentumRelease"),t.updateProgress(c),t.setTransition(e),t.setTranslate(c),t.transitionStart(!0,t.swipeDirection),t.animating||(t.animating=!0,n.transitionEnd((()=>{t&&!t.destroyed&&t.transitionEnd()})))):t.updateProgress(c),t.updateActiveIndex(),t.updateSlidesClasses()}else{if(r.freeMode.sticky)return void t.slideToClosest();r.freeMode&&a("_freeModeNoMomentumRelease")}(!r.freeMode.momentum||c>=r.longSwipesMs)&&(t.updateProgress(),t.updateActiveIndex(),t.updateSlidesClasses())}}}})},function(e){let t,s,a,{swiper:i,extendParams:r}=e;r({grid:{rows:1,fill:"column"}}),i.grid={initSlides:e=>{const{slidesPerView:r}=i.params,{rows:n,fill:l}=i.params.grid;s=t/n,a=Math.floor(e/n),t=Math.floor(e/n)===e/n?e:Math.ceil(e/n)*n,"auto"!==r&&"row"===l&&(t=Math.max(t,r*n))},updateSlide:(e,r,n,l)=>{const{slidesPerGroup:o,spaceBetween:d}=i.params,{rows:c,fill:p}=i.params.grid;let u,h,m;if("row"===p&&o>1){const s=Math.floor(e/(o*c)),a=e-c*o*s,i=0===s?o:Math.min(Math.ceil((n-s*c*o)/c),o);m=Math.floor(a/i),h=a-m*i+s*o,u=h+m*t/c,r.css({"-webkit-order":u,order:u})}else"column"===p?(h=Math.floor(e/c),m=e-h*c,(h>a||h===a&&m===c-1)&&(m+=1,m>=c&&(m=0,h+=1))):(m=Math.floor(e/s),h=e-m*s);r.css(l("margin-top"),0!==m?d&&`${d}px`:"")},updateWrapperSize:(e,s,a)=>{const{spaceBetween:r,centeredSlides:n,roundLengths:l}=i.params,{rows:o}=i.params.grid;if(i.virtualSize=(e+r)*t,i.virtualSize=Math.ceil(i.virtualSize/o)-r,i.$wrapperEl.css({[a("width")]:`${i.virtualSize+r}px`}),n){s.splice(0,s.length);const e=[];for(let t=0;t{const{slides:e}=t,s=t.params.fadeEffect;for(let a=0;a{const{transformEl:s}=t.params.fadeEffect;(s?t.slides.find(s):t.slides).transition(e),ae({swiper:t,duration:e,transformEl:s,allSlides:!0})},overwriteParams:()=>({slidesPerView:1,slidesPerGroup:1,watchSlidesProgress:!0,spaceBetween:0,virtualTranslate:!t.params.cssMode})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({cubeEffect:{slideShadows:!0,shadow:!0,shadowOffset:20,shadowScale:.94}});const i=(e,t,s)=>{let a=s?e.find(".swiper-slide-shadow-left"):e.find(".swiper-slide-shadow-top"),i=s?e.find(".swiper-slide-shadow-right"):e.find(".swiper-slide-shadow-bottom");0===a.length&&(a=d(`
`),e.append(a)),0===i.length&&(i=d(`
`),e.append(i)),a.length&&(a[0].style.opacity=Math.max(-t,0)),i.length&&(i[0].style.opacity=Math.max(t,0))};te({effect:"cube",swiper:t,on:a,setTranslate:()=>{const{$el:e,$wrapperEl:s,slides:a,width:r,height:n,rtlTranslate:l,size:o,browser:c}=t,p=t.params.cubeEffect,u=t.isHorizontal(),h=t.virtual&&t.params.virtual.enabled;let m,f=0;p.shadow&&(u?(m=s.find(".swiper-cube-shadow"),0===m.length&&(m=d('
'),s.append(m)),m.css({height:`${r}px`})):(m=e.find(".swiper-cube-shadow"),0===m.length&&(m=d('
'),e.append(m))));for(let e=0;e-1&&(f=90*s+90*d,l&&(f=90*-s-90*d)),t.transform(v),p.slideShadows&&i(t,d,u)}if(s.css({"-webkit-transform-origin":`50% 50% -${o/2}px`,"transform-origin":`50% 50% -${o/2}px`}),p.shadow)if(u)m.transform(`translate3d(0px, ${r/2+p.shadowOffset}px, ${-r/2}px) rotateX(90deg) rotateZ(0deg) scale(${p.shadowScale})`);else{const e=Math.abs(f)-90*Math.floor(Math.abs(f)/90),t=1.5-(Math.sin(2*e*Math.PI/360)/2+Math.cos(2*e*Math.PI/360)/2),s=p.shadowScale,a=p.shadowScale/t,i=p.shadowOffset;m.transform(`scale3d(${s}, 1, ${a}) translate3d(0px, ${n/2+i}px, ${-n/2/a}px) rotateX(-90deg)`)}const g=c.isSafari||c.isWebView?-o/2:0;s.transform(`translate3d(0px,0,${g}px) rotateX(${t.isHorizontal()?0:f}deg) rotateY(${t.isHorizontal()?-f:0}deg)`),s[0].style.setProperty("--swiper-cube-translate-z",`${g}px`)},setTransition:e=>{const{$el:s,slides:a}=t;a.transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e),t.params.cubeEffect.shadow&&!t.isHorizontal()&&s.find(".swiper-cube-shadow").transition(e)},recreateShadows:()=>{const e=t.isHorizontal();t.slides.each((t=>{const s=Math.max(Math.min(t.progress,1),-1);i(d(t),s,e)}))},getEffectParams:()=>t.params.cubeEffect,perspective:()=>!0,overwriteParams:()=>({slidesPerView:1,slidesPerGroup:1,watchSlidesProgress:!0,resistanceRatio:0,spaceBetween:0,centeredSlides:!1,virtualTranslate:!0})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({flipEffect:{slideShadows:!0,limitRotation:!0,transformEl:null}});const i=(e,s,a)=>{let i=t.isHorizontal()?e.find(".swiper-slide-shadow-left"):e.find(".swiper-slide-shadow-top"),r=t.isHorizontal()?e.find(".swiper-slide-shadow-right"):e.find(".swiper-slide-shadow-bottom");0===i.length&&(i=ie(a,e,t.isHorizontal()?"left":"top")),0===r.length&&(r=ie(a,e,t.isHorizontal()?"right":"bottom")),i.length&&(i[0].style.opacity=Math.max(-s,0)),r.length&&(r[0].style.opacity=Math.max(s,0))};te({effect:"flip",swiper:t,on:a,setTranslate:()=>{const{slides:e,rtlTranslate:s}=t,a=t.params.flipEffect;for(let r=0;r{const{transformEl:s}=t.params.flipEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e),ae({swiper:t,duration:e,transformEl:s})},recreateShadows:()=>{const e=t.params.flipEffect;t.slides.each((s=>{const a=d(s);let r=a[0].progress;t.params.flipEffect.limitRotation&&(r=Math.max(Math.min(s.progress,1),-1)),i(a,r,e)}))},getEffectParams:()=>t.params.flipEffect,perspective:()=>!0,overwriteParams:()=>({slidesPerView:1,slidesPerGroup:1,watchSlidesProgress:!0,spaceBetween:0,virtualTranslate:!t.params.cssMode})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({coverflowEffect:{rotate:50,stretch:0,depth:100,scale:1,modifier:1,slideShadows:!0,transformEl:null}}),te({effect:"coverflow",swiper:t,on:a,setTranslate:()=>{const{width:e,height:s,slides:a,slidesSizesGrid:i}=t,r=t.params.coverflowEffect,n=t.isHorizontal(),l=t.translate,o=n?e/2-l:s/2-l,d=n?r.rotate:-r.rotate,c=r.depth;for(let e=0,t=a.length;e0?p:0),s.length&&(s[0].style.opacity=-p>0?-p:0)}}},setTransition:e=>{const{transformEl:s}=t.params.coverflowEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e)},perspective:()=>!0,overwriteParams:()=>({watchSlidesProgress:!0})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({creativeEffect:{transformEl:null,limitProgress:1,shadowPerProgress:!1,progressMultiplier:1,perspective:!0,prev:{translate:[0,0,0],rotate:[0,0,0],opacity:1,scale:1},next:{translate:[0,0,0],rotate:[0,0,0],opacity:1,scale:1}}});const i=e=>"string"==typeof e?e:`${e}px`;te({effect:"creative",swiper:t,on:a,setTranslate:()=>{const{slides:e,$wrapperEl:s,slidesSizesGrid:a}=t,r=t.params.creativeEffect,{progressMultiplier:n}=r,l=t.params.centeredSlides;if(l){const e=a[0]/2-t.params.slidesOffsetBefore||0;s.transform(`translateX(calc(50% - ${e}px))`)}for(let s=0;s0&&(f=r.prev,m=!0),u.forEach(((e,t)=>{u[t]=`calc(${e}px + (${i(f.translate[t])} * ${Math.abs(d*n)}))`})),h.forEach(((e,t)=>{h[t]=f.rotate[t]*Math.abs(d*n)})),a[0].style.zIndex=-Math.abs(Math.round(o))+e.length;const g=u.join(", "),v=`rotateX(${h[0]}deg) rotateY(${h[1]}deg) rotateZ(${h[2]}deg)`,w=c<0?`scale(${1+(1-f.scale)*c*n})`:`scale(${1-(1-f.scale)*c*n})`,b=c<0?1+(1-f.opacity)*c*n:1-(1-f.opacity)*c*n,x=`translate3d(${g}) ${v} ${w}`;if(m&&f.shadow||!m){let e=a.children(".swiper-slide-shadow");if(0===e.length&&f.shadow&&(e=ie(r,a)),e.length){const t=r.shadowPerProgress?d*(1/r.limitProgress):d;e[0].style.opacity=Math.min(Math.max(Math.abs(t),0),1)}}const y=se(r,a);y.transform(x).css({opacity:b}),f.origin&&y.css("transform-origin",f.origin)}},setTransition:e=>{const{transformEl:s}=t.params.creativeEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow").transition(e),ae({swiper:t,duration:e,transformEl:s,allSlides:!0})},perspective:()=>t.params.creativeEffect.perspective,overwriteParams:()=>({watchSlidesProgress:!0,virtualTranslate:!t.params.cssMode})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({cardsEffect:{slideShadows:!0,transformEl:null,rotate:!0,perSlideRotate:2,perSlideOffset:8}}),te({effect:"cards",swiper:t,on:a,setTranslate:()=>{const{slides:e,activeIndex:s}=t,a=t.params.cardsEffect,{startTranslate:i,isTouched:r}=t.touchEventsData,n=t.translate;for(let l=0;l0&&c<1&&(r||t.params.cssMode)&&n-1&&(r||t.params.cssMode)&&n>i;if(b||x){const e=(1-Math.abs((Math.abs(c)-.5)/.5))**.5;g+=-28*c*e,f+=-.5*e,v+=96*e,h=-25*e*Math.abs(c)+"%"}if(u=c<0?`calc(${u}px + (${v*Math.abs(c)}%))`:c>0?`calc(${u}px + (-${v*Math.abs(c)}%))`:`${u}px`,!t.isHorizontal()){const e=h;h=u,u=e}const y=c<0?""+(1+(1-f)*c):""+(1-(1-f)*c),E=`\n translate3d(${u}, ${h}, ${m}px)\n rotateZ(${a.rotate?g:0}deg)\n scale(${y})\n `;if(a.slideShadows){let e=o.find(".swiper-slide-shadow");0===e.length&&(e=ie(a,o)),e.length&&(e[0].style.opacity=Math.min(Math.max((Math.abs(c)-.5)/.5,0),1))}o[0].style.zIndex=-Math.abs(Math.round(d))+e.length;se(a,o).transform(E)}},setTransition:e=>{const{transformEl:s}=t.params.cardsEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow").transition(e),ae({swiper:t,duration:e,transformEl:s})},perspective:()=>!0,overwriteParams:()=>({watchSlidesProgress:!0,virtualTranslate:!t.params.cssMode})})}];return V.use(re),V})); +//# sourceMappingURL=swiper-bundle.min.js.map \ No newline at end of file diff --git a/apps/smp-server/static/media/tailwind.css b/apps/smp-server/static/media/tailwind.css new file mode 100644 index 000000000..04ea69992 --- /dev/null +++ b/apps/smp-server/static/media/tailwind.css @@ -0,0 +1,3058 @@ +/* +! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.visible { + visibility: visible; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.-left-10 { + left: -2.5rem; +} + +.bottom-0 { + bottom: 0px; +} + +.bottom-\[80px\] { + bottom: 80px; +} + +.left-0 { + left: 0px; +} + +.left-1 { + left: 0.25rem; +} + +.left-\[-3px\] { + left: -3px; +} + +.right-0 { + right: 0px; +} + +.right-1 { + right: 0.25rem; +} + +.right-\[-6px\] { + right: -6px; +} + +.top-0 { + top: 0px; +} + +.top-10 { + top: 2.5rem; +} + +.top-11 { + top: 2.75rem; +} + +.top-\[52\%\] { + top: 52%; +} + +.top-\[66px\] { + top: 66px; +} + +.top-full { + top: 100%; +} + +.z-10 { + z-index: 10; +} + +.z-50 { + z-index: 50; +} + +.z-\[10000\] { + z-index: 10000; +} + +.z-\[10001\] { + z-index: 10001; +} + +.z-\[49\] { + z-index: 49; +} + +.float-right { + float: right; +} + +.m-1 { + margin: 0.25rem; +} + +.m-auto { + margin: auto; +} + +.\!my-4 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.mx-5 { + margin-left: 1.25rem; + margin-right: 1.25rem; +} + +.my-10 { + margin-top: 2.5rem; + margin-bottom: 2.5rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-5 { + margin-top: 1.25rem; + margin-bottom: 1.25rem; +} + +.my-\[40px\] { + margin-top: 40px; + margin-bottom: 40px; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-14 { + margin-bottom: 3.5rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-20 { + margin-bottom: 5rem; +} + +.mb-24 { + margin-bottom: 6rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-32 { + margin-bottom: 8rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-7 { + margin-bottom: 1.75rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mb-9 { + margin-bottom: 2.25rem; +} + +.mb-\[11px\] { + margin-bottom: 11px; +} + +.mb-\[12px\] { + margin-bottom: 12px; +} + +.mb-\[16px\] { + margin-bottom: 16px; +} + +.mb-\[20px\] { + margin-bottom: 20px; +} + +.mb-\[24px\] { + margin-bottom: 24px; +} + +.mb-\[28px\] { + margin-bottom: 28px; +} + +.mb-\[30px\] { + margin-bottom: 30px; +} + +.mb-\[32px\] { + margin-bottom: 32px; +} + +.mb-\[36px\] { + margin-bottom: 36px; +} + +.mb-\[40px\] { + margin-bottom: 40px; +} + +.mb-\[46px\] { + margin-bottom: 46px; +} + +.mb-\[50px\] { + margin-bottom: 50px; +} + +.mb-\[54px\] { + margin-bottom: 54px; +} + +.mb-\[62px\] { + margin-bottom: 62px; +} + +.mb-\[74px\] { + margin-bottom: 74px; +} + +.mb-\[75px\] { + margin-bottom: 75px; +} + +.mb-\[76px\] { + margin-bottom: 76px; +} + +.mb-\[80px\] { + margin-bottom: 80px; +} + +.ml-\[-15px\] { + margin-left: -15px; +} + +.ml-\[-6px\] { + margin-left: -6px; +} + +.mr-\[-4px\] { + margin-right: -4px; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-\[-100px\] { + margin-top: -100px; +} + +.mt-\[10px\] { + margin-top: 10px; +} + +.mt-\[14px\] { + margin-top: 14px; +} + +.mt-\[30px\] { + margin-top: 30px; +} + +.mt-\[60px\] { + margin-top: 60px; +} + +.mt-\[66px\] { + margin-top: 66px; +} + +.mt-\[74px\] { + margin-top: 74px; +} + +.mt-\[8px\] { + margin-top: 8px; +} + +.mt-auto { + margin-top: auto; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-1 { + height: 0.25rem; +} + +.h-11 { + height: 2.75rem; +} + +.h-4 { + height: 1rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-\[110px\] { + height: 110px; +} + +.h-\[131px\] { + height: 131px; +} + +.h-\[138px\] { + height: 138px; +} + +.h-\[180px\] { + height: 180px; +} + +.h-\[1px\] { + height: 1px; +} + +.h-\[215px\] { + height: 215px; +} + +.h-\[322px\] { + height: 322px; +} + +.h-\[32px\] { + height: 32px; +} + +.h-\[340px\] { + height: 340px; +} + +.h-\[34px\] { + height: 34px; +} + +.h-\[36px\] { + height: 36px; +} + +.h-\[40px\] { + height: 40px; +} + +.h-\[44px\] { + height: 44px; +} + +.h-\[46px\] { + height: 46px; +} + +.h-\[480px\] { + height: 480px; +} + +.h-\[52px\] { + height: 52px; +} + +.h-\[55\%\] { + height: 55%; +} + +.h-\[66px\] { + height: 66px; +} + +.h-\[80px\] { + height: 80px; +} + +.h-\[calc\(100\%-81\.42px\)\] { + height: calc(100% - 81.42px); +} + +.h-\[inherit\] { + height: inherit; +} + +.h-auto { + height: auto; +} + +.h-fit { + height: -moz-fit-content; + height: fit-content; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + +.max-h-\[226px\] { + max-height: 226px; +} + +.max-h-\[50px\] { + max-height: 50px; +} + +.min-h-\[131px\] { + min-height: 131px; +} + +.min-h-\[200px\] { + min-height: 200px; +} + +.min-h-\[620px\] { + min-height: 620px; +} + +.min-h-\[inherit\] { + min-height: inherit; +} + +.w-4 { + width: 1rem; +} + +.w-8 { + width: 2rem; +} + +.w-\[175px\] { + width: 175px; +} + +.w-\[235px\] { + width: 235px; +} + +.w-\[238px\] { + width: 238px; +} + +.w-\[36px\] { + width: 36px; +} + +.w-\[44px\] { + width: 44px; +} + +.w-\[630px\] { + width: 630px; +} + +.w-\[inherit\] { + width: inherit; +} + +.w-auto { + width: auto; +} + +.w-full { + width: 100%; +} + +.min-w-\[152px\] { + min-width: 152px; +} + +.min-w-\[170px\] { + min-width: 170px; +} + +.min-w-\[180px\] { + min-width: 180px; +} + +.min-w-\[210px\] { + min-width: 210px; +} + +.min-w-\[300px\] { + min-width: 300px; +} + +.max-w-\[223px\] { + max-width: 223px; +} + +.max-w-\[230px\] { + max-width: 230px; +} + +.max-w-\[234px\] { + max-width: 234px; +} + +.max-w-\[240px\] { + max-width: 240px; +} + +.max-w-\[265px\] { + max-width: 265px; +} + +.max-w-\[280px\] { + max-width: 280px; +} + +.max-w-\[294px\] { + max-width: 294px; +} + +.max-w-\[400px\] { + max-width: 400px; +} + +.max-w-\[448px\] { + max-width: 448px; +} + +.max-w-\[468px\] { + max-width: 468px; +} + +.max-w-\[475px\] { + max-width: 475px; +} + +.max-w-\[500px\] { + max-width: 500px; +} + +.max-w-\[540px\] { + max-width: 540px; +} + +.max-w-\[541px\] { + max-width: 541px; +} + +.max-w-\[602px\] { + max-width: 602px; +} + +.max-w-\[617px\] { + max-width: 617px; +} + +.max-w-\[900px\] { + max-width: 900px; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-\[1\] { + flex: 1; +} + +.flex-\[2\.5\] { + flex: 2.5; +} + +.border-separate { + border-collapse: separate; +} + +.border-spacing-x-5 { + --tw-border-spacing-x: 1.25rem; + border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); +} + +.border-spacing-y-2 { + --tw-border-spacing-y: 0.5rem; + border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); +} + +.translate-x-\[-30\%\] { + --tw-translate-x: -30%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-\[-30\%\] { + --tw-translate-y: -30%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-pointer { + cursor: pointer; +} + +.resize { + resize: both; +} + +.list-inside { + list-style-position: inside; +} + +.list-decimal { + list-style-type: decimal; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-col-reverse { + flex-direction: column-reverse; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-10 { + gap: 2.5rem; +} + +.gap-12 { + gap: 3rem; +} + +.gap-16 { + gap: 4rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-5 { + gap: 1.25rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-\[150px\] { + gap: 150px; +} + +.gap-\[20px\] { + gap: 20px; +} + +.gap-\[50px\] { + gap: 50px; +} + +.gap-x-10 { + -moz-column-gap: 2.5rem; + column-gap: 2.5rem; +} + +.gap-y-32 { + row-gap: 8rem; +} + +.self-center { + align-self: center; +} + +.self-stretch { + align-self: stretch; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.break-words { + overflow-wrap: break-word; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-\[12px\] { + border-radius: 12px; +} + +.rounded-\[20px\] { + border-radius: 20px; +} + +.rounded-\[30px\] { + border-radius: 30px; +} + +.rounded-\[34px\] { + border-radius: 34px; +} + +.rounded-\[4px\] { + border-radius: 4px; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-b-\[20px\] { + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; +} + +.border { + border-width: 1px; +} + +.border-\[1px\] { + border-width: 1px; +} + +.border-solid { + border-style: solid; +} + +.border-\[\#0053D0\] { + --tw-border-opacity: 1; + border-color: rgb(0 83 208 / var(--tw-border-opacity)); +} + +.border-\[\#859096\] { + --tw-border-opacity: 1; + border-color: rgb(133 144 150 / var(--tw-border-opacity)); +} + +.border-\[\#A8B0B4\] { + --tw-border-opacity: 1; + border-color: rgb(168 176 180 / var(--tw-border-opacity)); +} + +.border-primary-light { + --tw-border-opacity: 1; + border-color: rgb(0 83 208 / var(--tw-border-opacity)); +} + +.border-opacity-60 { + --tw-border-opacity: 0.6; +} + +.bg-\[\#0053D0\] { + --tw-bg-opacity: 1; + background-color: rgb(0 83 208 / var(--tw-bg-opacity)); +} + +.bg-\[\#0197FF\] { + --tw-bg-opacity: 1; + background-color: rgb(1 151 255 / var(--tw-bg-opacity)); +} + +.bg-\[\#17203D\] { + --tw-bg-opacity: 1; + background-color: rgb(23 32 61 / var(--tw-bg-opacity)); +} + +.bg-\[\#48F6C2\] { + --tw-bg-opacity: 1; + background-color: rgb(72 246 194 / var(--tw-bg-opacity)); +} + +.bg-\[\#D9E7ED\] { + --tw-bg-opacity: 1; + background-color: rgb(217 231 237 / var(--tw-bg-opacity)); +} + +.bg-\[\#D9ECFF\] { + --tw-bg-opacity: 1; + background-color: rgb(217 236 255 / var(--tw-bg-opacity)); +} + +.bg-\[\#F0F1F2\] { + --tw-bg-opacity: 1; + background-color: rgb(240 241 242 / var(--tw-bg-opacity)); +} + +.bg-\[\#F3F6F7\] { + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); +} + +.bg-\[\#fff\] { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); +} + +.bg-card-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-card-desc-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(217 231 237 / var(--tw-bg-opacity)); +} + +.bg-primary-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-primary-light { + --tw-bg-opacity: 1; + background-color: rgb(0 83 208 / var(--tw-bg-opacity)); +} + +.bg-secondary-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-yellow-200 { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); +} + +.fill-\[rgb\(60\2c 60\2c 60\)\] { + fill: rgb(60,60,60); +} + +.fill-black { + fill: #0D0E12; +} + +.fill-grey-black { + fill: #3F484B; +} + +.fill-primary-light { + fill: #0053D0; +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.\!py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.px-0 { + padding-left: 0px; + padding-right: 0px; +} + +.px-20 { + padding-left: 5rem; + padding-right: 5rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.px-\[20px\] { + padding-left: 20px; + padding-right: 20px; +} + +.px-\[34px\] { + padding-left: 34px; + padding-right: 34px; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.py-\[20px\] { + padding-top: 20px; + padding-bottom: 20px; +} + +.py-\[24px\] { + padding-top: 24px; + padding-bottom: 24px; +} + +.py-\[50px\] { + padding-top: 50px; + padding-bottom: 50px; +} + +.py-\[70px\] { + padding-top: 70px; + padding-bottom: 70px; +} + +.py-\[75px\] { + padding-top: 75px; + padding-bottom: 75px; +} + +.py-\[90px\] { + padding-top: 90px; + padding-bottom: 90px; +} + +.py-\[95px\] { + padding-top: 95px; + padding-bottom: 95px; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-8 { + padding-bottom: 2rem; +} + +.pb-\[90px\] { + padding-bottom: 90px; +} + +.pr-10 { + padding-right: 2.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-5 { + padding-top: 1.25rem; +} + +.pt-\[106px\] { + padding-top: 106px; +} + +.pt-\[30px\] { + padding-top: 30px; +} + +.pt-\[40px\] { + padding-top: 40px; +} + +.pt-\[66px\] { + padding-top: 66px; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-\[12px\] { + font-size: 12px; +} + +.text-\[14px\] { + font-size: 14px; +} + +.text-\[16px\] { + font-size: 16px; +} + +.text-\[18px\] { + font-size: 18px; +} + +.text-\[20px\] { + font-size: 20px; +} + +.text-\[24px\] { + font-size: 24px; +} + +.text-\[25px\] { + font-size: 25px; +} + +.text-\[28px\] { + font-size: 28px; +} + +.text-\[30px\] { + font-size: 30px; +} + +.text-\[35px\] { + font-size: 35px; +} + +.text-\[38px\] { + font-size: 38px; +} + +.text-\[42px\] { + font-size: 42px; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.font-medium { + font-weight: 500; +} + +.font-normal { + font-weight: 400; +} + +.capitalize { + text-transform: capitalize; +} + +.italic { + font-style: italic; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-\[19px\] { + line-height: 19px; +} + +.leading-\[24px\] { + line-height: 24px; +} + +.leading-\[26px\] { + line-height: 26px; +} + +.leading-\[28px\] { + line-height: 28px; +} + +.leading-\[33px\] { + line-height: 33px; +} + +.leading-\[34px\] { + line-height: 34px; +} + +.leading-\[36px\] { + line-height: 36px; +} + +.leading-\[43px\] { + line-height: 43px; +} + +.leading-\[45px\] { + line-height: 45px; +} + +.leading-\[46px\] { + line-height: 46px; +} + +.tracking-\[0\.01em\] { + letter-spacing: 0.01em; +} + +.tracking-\[0\.02em\] { + letter-spacing: 0.02em; +} + +.tracking-\[0\.03em\] { + letter-spacing: 0.03em; +} + +.tracking-\[0\.04em\] { + letter-spacing: 0.04em; +} + +.tracking-\[0\.06em\] { + letter-spacing: 0.06em; +} + +.\!text-\[rgb\(60\2c 60\2c 60\)\] { + --tw-text-opacity: 1 !important; + color: rgb(60 60 60 / var(--tw-text-opacity)) !important; +} + +.\!text-primary-pressed-light { + --tw-text-opacity: 1 !important; + color: rgb(64 122 210 / var(--tw-text-opacity)) !important; +} + +.text-\[\#606C71\] { + --tw-text-opacity: 1; + color: rgb(96 108 113 / var(--tw-text-opacity)); +} + +.text-\[\#A8B0B4\] { + --tw-text-opacity: 1; + color: rgb(168 176 180 / var(--tw-text-opacity)); +} + +.text-\[\#DD0000\] { + --tw-text-opacity: 1; + color: rgb(221 0 0 / var(--tw-text-opacity)); +} + +.text-active-blue { + --tw-text-opacity: 1; + color: rgb(1 151 255 / var(--tw-text-opacity)); +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(13 14 18 / var(--tw-text-opacity)); +} + +.text-grey-black { + --tw-text-opacity: 1; + color: rgb(63 72 75 / var(--tw-text-opacity)); +} + +.text-primary-light { + --tw-text-opacity: 1; + color: rgb(0 83 208 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.underline { + text-decoration-line: underline; +} + +.\!no-underline { + text-decoration-line: none !important; +} + +.no-underline { + text-decoration-line: none; +} + +.underline-offset-2 { + text-underline-offset: 2px; +} + +.underline-offset-4 { + text-underline-offset: 4px; +} + +.opacity-100 { + opacity: 1; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0_0_3px_rgb\(60_72_88_\/_15\%\)\] { + --tw-shadow: 0 0 3px rgb(60 72 88 / 15%); + --tw-shadow-colored: 0 0 3px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0px_20px_30px_rgba\(0\2c 0\2c 0\2c 0\.12\)\] { + --tw-shadow: 0px 20px 30px rgba(0,0,0,0.12); + --tw-shadow-colored: 0px 20px 30px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0px_3px_12px_rgba\(0\2c 0\2c 0\2c 0\.2\)\] { + --tw-shadow: 0px 3px 12px rgba(0,0,0,0.2); + --tw-shadow-colored: 0px 3px 12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0px_50px_61px_rgba\(0\2c 0\2c 0\2c 0\.12\)\] { + --tw-shadow: 0px 50px 61px rgba(0,0,0,0.12); + --tw-shadow-colored: 0px 50px 61px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[2px_2px_0px_\#859096\] { + --tw-shadow: 2px 2px 0px #859096; + --tw-shadow-colored: 2px 2px 0px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[inset_0px_2px_2px_rgba\(0\2c 0\2c 0\2c 0\.15\)\] { + --tw-shadow: inset 0px 2px 2px rgba(0,0,0,0.15); + --tw-shadow-colored: inset 0px 2px 2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.invert { + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.placeholder\:text-base::-moz-placeholder { + font-size: 1rem; + line-height: 1.5rem; +} + +.placeholder\:text-base::placeholder { + font-size: 1rem; + line-height: 1.5rem; +} + +.placeholder\:font-normal::-moz-placeholder { + font-weight: 400; +} + +.placeholder\:font-normal::placeholder { + font-weight: 400; +} + +.placeholder\:tracking-\[0\.01em\]::-moz-placeholder { + letter-spacing: 0.01em; +} + +.placeholder\:tracking-\[0\.01em\]::placeholder { + letter-spacing: 0.01em; +} + +.placeholder\:text-grey-black::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(63 72 75 / var(--tw-text-opacity)); +} + +.placeholder\:text-grey-black::placeholder { + --tw-text-opacity: 1; + color: rgb(63 72 75 / var(--tw-text-opacity)); +} + +.before\:absolute::before { + content: var(--tw-content); + position: absolute; +} + +.before\:h-full::before { + content: var(--tw-content); + height: 100%; +} + +.before\:w-full::before { + content: var(--tw-content); + width: 100%; +} + +.before\:bg-black::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); +} + +.before\:bg-secondary-bg-light::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); +} + +.before\:opacity-90::before { + content: var(--tw-content); + opacity: 0.9; +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +:is([dir="ltr"] .ltr\:ml-8) { + margin-left: 2rem; +} + +:is([dir="ltr"] .ltr\:mr-auto) { + margin-right: auto; +} + +:is([dir="ltr"] .ltr\:\!hidden) { + display: none !important; +} + +:is([dir="ltr"] .ltr\:rounded-l-\[34px\]) { + border-top-left-radius: 34px; + border-bottom-left-radius: 34px; +} + +:is([dir="ltr"] .ltr\:rounded-r-\[34px\]) { + border-top-right-radius: 34px; + border-bottom-right-radius: 34px; +} + +:is([dir="rtl"] .rtl\:ml-auto) { + margin-left: auto; +} + +:is([dir="rtl"] .rtl\:mr-8) { + margin-right: 2rem; +} + +:is([dir="rtl"] .rtl\:\!hidden) { + display: none !important; +} + +:is([dir="rtl"] .rtl\:rounded-l-\[34px\]) { + border-top-left-radius: 34px; + border-bottom-left-radius: 34px; +} + +:is([dir="rtl"] .rtl\:rounded-r-\[34px\]) { + border-top-right-radius: 34px; + border-bottom-right-radius: 34px; +} + +:is([dir="rtl"] .rtl\:text-right) { + text-align: right; +} + +:is(.dark .dark\:block) { + display: block; +} + +:is(.dark .dark\:inline-block) { + display: inline-block; +} + +:is(.dark .dark\:flex) { + display: flex; +} + +:is(.dark .dark\:\!hidden) { + display: none !important; +} + +:is(.dark .dark\:hidden) { + display: none; +} + +:is(.dark .dark\:rounded-\[6px\]) { + border-radius: 6px; +} + +:is(.dark .dark\:border) { + border-width: 1px; +} + +:is(.dark .dark\:border-none) { + border-style: none; +} + +:is(.dark .dark\:border-primary-dark) { + --tw-border-opacity: 1; + border-color: rgb(112 240 249 / var(--tw-border-opacity)); +} + +:is(.dark .dark\:border-white) { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + +:is(.dark .dark\:bg-\[\#0C0B13\]) { + --tw-bg-opacity: 1; + background-color: rgb(12 11 19 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#0E2B57\]) { + --tw-bg-opacity: 1; + background-color: rgb(14 43 87 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#11182F\]) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 47 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#171F3A\]) { + --tw-bg-opacity: 1; + background-color: rgb(23 31 58 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#17203D\]) { + --tw-bg-opacity: 1; + background-color: rgb(23 32 61 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[rgba\(112\2c 240\2c 249\2c 0\.2\)\]) { + background-color: rgba(112,240,249,0.2); +} + +:is(.dark .dark\:bg-black) { + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-card-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(23 32 61 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-card-desc-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(27 50 92 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-grey-black) { + --tw-bg-opacity: 1; + background-color: rgb(63 72 75 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-primary-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(12 11 19 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-primary-dark) { + --tw-bg-opacity: 1; + background-color: rgb(112 240 249 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-secondary-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 47 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-transparent) { + background-color: transparent; +} + +:is(.dark .dark\:bg-opacity-\[0\.2\]) { + --tw-bg-opacity: 0.2; +} + +:is(.dark .dark\:bg-gradient-radial-mobile) { + background-image: radial-gradient(77.4% 73.09% at -3.68% 100%, #17203D 0%, #0C0B13 100%); +} + +:is(.dark .dark\:fill-primary-dark) { + fill: #70F0F9; +} + +:is(.dark .dark\:fill-white) { + fill: #fff; +} + +:is(.dark .dark\:\!text-white) { + --tw-text-opacity: 1 !important; + color: rgb(255 255 255 / var(--tw-text-opacity)) !important; +} + +:is(.dark .dark\:text-\[\#70F0F9\]) { + --tw-text-opacity: 1; + color: rgb(112 240 249 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-black) { + --tw-text-opacity: 1; + color: rgb(13 14 18 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-primary-dark) { + --tw-text-opacity: 1; + color: rgb(112 240 249 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-white) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:opacity-\[0\.1\]) { + opacity: 0.1; +} + +:is(.dark .dark\:opacity-\[0\.2\]) { + opacity: 0.2; +} + +:is(.dark .dark\:shadow-none) { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +:is(.dark .placeholder\:dark\:text-white)::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .placeholder\:dark\:text-white)::placeholder { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:before\:bg-primary-bg-dark)::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(12 11 19 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:before\:bg-white)::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +@media (min-width: 640px) { + .sm\:block { + display: block; + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:p-14 { + padding: 3.5rem; + } + + .sm\:px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + + .sm\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } +} + +@media (min-width: 768px) { + .md\:static { + position: static; + } + + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:mb-16 { + margin-bottom: 4rem; + } + + .md\:mb-6 { + margin-bottom: 1.5rem; + } + + .md\:mb-8 { + margin-bottom: 2rem; + } + + .md\:mt-6 { + margin-top: 1.5rem; + } + + .md\:block { + display: block; + } + + .md\:inline-block { + display: inline-block; + } + + .md\:flex { + display: flex; + } + + .md\:hidden { + display: none; + } + + .md\:h-fit { + height: -moz-fit-content; + height: fit-content; + } + + .md\:max-h-\[660px\] { + max-height: 660px; + } + + .md\:min-h-fit { + min-height: -moz-fit-content; + min-height: fit-content; + } + + .md\:w-\[168px\] { + width: 168px; + } + + .md\:w-\[30\%\] { + width: 30%; + } + + .md\:w-\[70\%\] { + width: 70%; + } + + .md\:w-fit { + width: -moz-fit-content; + width: fit-content; + } + + .md\:max-w-\[1276px\] { + max-width: 1276px; + } + + .md\:max-w-\[220px\] { + max-width: 220px; + } + + .md\:max-w-\[490px\] { + max-width: 490px; + } + + .md\:flex-1 { + flex: 1 1 0%; + } + + .md\:flex-\[1\] { + flex: 1; + } + + .md\:flex-\[2\] { + flex: 2; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:flex-col-reverse { + flex-direction: column-reverse; + } + + .md\:items-start { + align-items: flex-start; + } + + .md\:items-center { + align-items: center; + } + + .md\:gap-14 { + gap: 3.5rem; + } + + .md\:gap-9 { + gap: 2.25rem; + } + + .md\:p-10 { + padding: 2.5rem; + } + + .md\:p-\[60px\] { + padding: 60px; + } + + .md\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .md\:px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; + } + + .md\:py-16 { + padding-top: 4rem; + padding-bottom: 4rem; + } + + .md\:py-7 { + padding-top: 1.75rem; + padding-bottom: 1.75rem; + } + + .md\:py-8 { + padding-top: 2rem; + padding-bottom: 2rem; + } + + .md\:text-left { + text-align: left; + } + + .md\:text-\[20px\] { + font-size: 20px; + } + + .md\:text-\[35px\] { + font-size: 35px; + } + + .md\:text-\[38px\] { + font-size: 38px; + } + + .md\:text-\[55px\] { + font-size: 55px; + } + + .md\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .md\:leading-\[43px\] { + line-height: 43px; + } + + .md\:leading-\[55px\] { + line-height: 55px; + } + + .md\:leading-\[63px\] { + line-height: 63px; + } + + :is([dir="rtl"] .md\:rtl\:inline-block) { + display: inline-block; + } + + :is([dir="rtl"] .md\:rtl\:text-right) { + text-align: right; + } + + :is(.dark .dark\:md\:inline-block) { + display: inline-block; + } + + :is(.dark .md\:dark\:inline-block) { + display: inline-block; + } +} + +@media (min-width: 1024px) { + .lg\:absolute { + position: absolute; + } + + .lg\:relative { + position: relative; + } + + .lg\:top-0 { + top: 0px; + } + + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:mb-20 { + margin-bottom: 5rem; + } + + .lg\:mb-8 { + margin-bottom: 2rem; + } + + .lg\:mb-\[54px\] { + margin-bottom: 54px; + } + + .lg\:mb-\[90px\] { + margin-bottom: 90px; + } + + .lg\:mt-0 { + margin-top: 0px; + } + + .lg\:mt-\[10px\] { + margin-top: 10px; + } + + .lg\:flex { + display: flex; + } + + .lg\:hidden { + display: none; + } + + .lg\:h-0 { + height: 0px; + } + + .lg\:h-\[642px\] { + height: 642px; + } + + .lg\:h-\[855px\] { + height: 855px; + } + + .lg\:h-\[888px\] { + height: 888px; + } + + .lg\:h-\[890px\] { + height: 890px; + } + + .lg\:h-\[950px\] { + height: 950px; + } + + .lg\:h-\[calc\(100vh-66px\)\] { + height: calc(100vh - 66px); + } + + .lg\:h-auto { + height: auto; + } + + .lg\:h-fit { + height: -moz-fit-content; + height: fit-content; + } + + .lg\:max-h-\[888px\] { + max-height: 888px; + } + + .lg\:w-2\/5 { + width: 40%; + } + + .lg\:w-3\/5 { + width: 60%; + } + + .lg\:w-auto { + width: auto; + } + + .lg\:w-full { + width: 100%; + } + + .lg\:max-w-\[240px\] { + max-width: 240px; + } + + .lg\:max-w-\[448px\] { + max-width: 448px; + } + + .lg\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:flex-col { + flex-direction: column; + } + + .lg\:items-start { + align-items: flex-start; + } + + .lg\:items-center { + align-items: center; + } + + .lg\:gap-0 { + gap: 0px; + } + + .lg\:gap-16 { + gap: 4rem; + } + + .lg\:gap-28 { + gap: 7rem; + } + + .lg\:gap-5 { + gap: 1.25rem; + } + + .lg\:gap-\[350px\] { + gap: 350px; + } + + .lg\:bg-transparent { + background-color: transparent; + } + + .lg\:bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + } + + .lg\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .lg\:px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; + } + + .lg\:px-7 { + padding-left: 1.75rem; + padding-right: 1.75rem; + } + + .lg\:px-\[20px\] { + padding-left: 20px; + padding-right: 20px; + } + + .lg\:px-\[50px\] { + padding-left: 50px; + padding-right: 50px; + } + + .lg\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .lg\:py-4 { + padding-top: 1rem; + padding-bottom: 1rem; + } + + .lg\:py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; + } + + .lg\:py-\[50px\] { + padding-top: 50px; + padding-bottom: 50px; + } + + .lg\:pt-0 { + padding-top: 0px; + } + + .lg\:text-center { + text-align: center; + } + + .lg\:text-\[32px\] { + font-size: 32px; + } + + .lg\:text-\[35px\] { + font-size: 35px; + } + + .lg\:text-\[38px\] { + font-size: 38px; + } + + .lg\:text-\[45px\] { + font-size: 45px; + } + + .lg\:text-\[65px\] { + font-size: 65px; + } + + .lg\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .lg\:leading-\[36px\] { + line-height: 36px; + } + + .lg\:leading-\[45px\] { + line-height: 45px; + } + + .lg\:shadow-\[0_0_3px_rgb\(60_72_88_\/_15\%\)\] { + --tw-shadow: 0 0 3px rgb(60 72 88 / 15%); + --tw-shadow-colored: 0 0 3px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + :is([dir="ltr"] .ltr\:lg\:ml-4) { + margin-left: 1rem; + } + + :is([dir="ltr"] .ltr\:lg\:ml-5) { + margin-left: 1.25rem; + } + + :is([dir="ltr"] .lg\:ltr\:text-left) { + text-align: left; + } + + :is([dir="rtl"] .rtl\:lg\:mr-4) { + margin-right: 1rem; + } + + :is([dir="rtl"] .rtl\:lg\:mr-5) { + margin-right: 1.25rem; + } + + :is([dir="rtl"] .lg\:rtl\:text-right) { + text-align: right; + } + + :is(.dark .dark\:lg\:flex) { + display: flex; + } + + :is(.dark .dark\:lg\:hidden) { + display: none; + } + + :is(.dark .dark\:lg\:bg-black) { + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); + } + + :is(.dark .dark\:lg\:bg-gradient-radial) { + background-image: radial-gradient(88.77% 102.03% at 92.64% -13.22%, #17203D 0%, #0C0B13 100%); + } + + :is(.dark .dark\:lg\:bg-none) { + background-image: none; + } +} + +@media (min-width: 1280px) { + .xl\:absolute { + position: absolute; + } + + .xl\:sticky { + position: sticky; + } + + .xl\:mb-0 { + margin-bottom: 0px; + } + + .xl\:mb-8 { + margin-bottom: 2rem; + } + + .xl\:mb-\[25px\] { + margin-bottom: 25px; + } + + .xl\:block { + display: block; + } + + .xl\:flex { + display: flex; + } + + .xl\:hidden { + display: none; + } + + .xl\:h-\[888px\] { + height: 888px; + } + + .xl\:h-\[calc\(100vh-66px\)\] { + height: calc(100vh - 66px); + } + + .xl\:h-full { + height: 100%; + } + + .xl\:max-h-\[888px\] { + max-height: 888px; + } + + .xl\:min-h-\[565px\] { + min-height: 565px; + } + + .xl\:min-w-\[600px\] { + min-width: 600px; + } + + .xl\:max-w-\[600px\] { + max-width: 600px; + } + + .xl\:flex-row { + flex-direction: row; + } + + .xl\:flex-row-reverse { + flex-direction: row-reverse; + } + + .xl\:items-start { + align-items: flex-start; + } + + .xl\:items-center { + align-items: center; + } + + .xl\:justify-start { + justify-content: flex-start; + } + + .xl\:justify-between { + justify-content: space-between; + } + + .xl\:justify-around { + justify-content: space-around; + } + + .xl\:gap-10 { + gap: 2.5rem; + } + + .xl\:gap-8 { + gap: 2rem; + } + + .xl\:text-left { + text-align: left; + } + + .xl\:text-justify { + text-align: justify; + } + + .xl\:text-\[16px\] { + font-size: 16px; + } + + .xl\:bg-secondary-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); + } + + :is([dir="ltr"] .ltr\:xl\:ml-10) { + margin-left: 2.5rem; + } + + :is([dir="ltr"] .ltr\:xl\:ml-8) { + margin-left: 2rem; + } + + :is([dir="rtl"] .rtl\:xl\:mr-10) { + margin-right: 2.5rem; + } + + :is([dir="rtl"] .rtl\:xl\:mr-8) { + margin-right: 2rem; + } + + :is([dir="rtl"] .xl\:rtl\:text-right) { + text-align: right; + } + + :is(.dark .dark\:xl\:bg-secondary-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 47 / var(--tw-bg-opacity)); + } +} diff --git a/apps/smp-server/static/media/testflight.png b/apps/smp-server/static/media/testflight.png new file mode 100644 index 0000000000000000000000000000000000000000..8111a69d548505d1a6d7a587f3c7465368dbddd8 GIT binary patch literal 18151 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 IO () +serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do + forM_ webHttpPort $ \port -> flip forkFinally (\e -> logError $ "HTTP server crashed: " <> tshow e) $ do + logInfo $ "Serving static site on port " <> tshow port + W.runSettings (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath) + forM_ webHttpsParams $ \WebHttpsParams {port, cert, key} -> flip forkFinally (\e -> logError $ "HTTPS server crashed: " <> tshow e) $ do + logInfo $ "Serving static site on port " <> tshow port <> " (TLS)" + W.runTLS (W.tlsSettings cert key) (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath) + where + mkSettings port = setPort port defaultSettings + +generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO () +generateSite si onionHost sitePath = do + createDirectoryIfMissing True sitePath + B.writeFile (sitePath "index.html") $ serverInformation si onionHost + createDirectoryIfMissing True $ sitePath "media" + forM_ E.mediaContent $ \(path, bs) -> B.writeFile (sitePath "media" path) bs + createDirectoryIfMissing True $ sitePath "contact" + B.writeFile (sitePath "contact" "index.html") E.linkHtml + createDirectoryIfMissing True $ sitePath "invitation" + B.writeFile (sitePath "invitation" "index.html") E.linkHtml + logInfo $ "Generated static site contents at " <> tshow sitePath + +serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString +serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs + where + substs = substConfig <> maybe [] substInfo information <> [("onionHost", strEncode <$> onionHost)] + substConfig = + [ ( "persistence", + Just $ case persistence config of + SPMMemoryOnly -> "In-memory only" + SPMQueues -> "Queues" + SPMMessages -> "Queues and messages" + ), + ("messageExpiration", Just $ maybe "Never" (fromString . timedTTLText) $ messageExpiration config), + ("statsEnabled", Just . yesNo $ statsEnabled config), + ("newQueuesAllowed", Just . yesNo $ newQueuesAllowed config), + ("basicAuthEnabled", Just . yesNo $ basicAuthEnabled config) + ] + yesNo True = "Yes" + yesNo False = "No" + substInfo spi = + concat + [ basic, + maybe [("usageConditions", Nothing), ("usageAmendments", Nothing)] conds (usageConditions spi), + maybe [("operator", Nothing)] operatorE (operator spi), + maybe [("admin", Nothing)] admin (adminContacts spi), + maybe [("complaints", Nothing)] complaints (complaintsContacts spi), + maybe [("hosting", Nothing)] hostingE (hosting spi), + server + ] + where + basic = + [ ("sourceCode", Just . encodeUtf8 $ sourceCode spi), + ("website", encodeUtf8 <$> website spi) + ] + conds ServerConditions {conditions, amendments} = + [ ("usageConditions", Just $ encodeUtf8 conditions), + ("usageAmendments", encodeUtf8 <$> amendments) + ] + operatorE Entity {name, country} = + [ ("operator", Just ""), + ("operatorEntity", Just $ encodeUtf8 name), + ("operatorCountry", encodeUtf8 <$> country) + ] + admin ServerContactAddress {simplex, email, pgp} = + [ ("admin", Just ""), + ("adminSimplex", strEncode <$> simplex), + ("adminEmail", encodeUtf8 <$> email), + ("adminPGP", encodeUtf8 . pkURI <$> pgp), + ("adminPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp) + ] + complaints ServerContactAddress {simplex, email, pgp} = + [ ("complaints", Just ""), + ("complaintsSimplex", strEncode <$> simplex), + ("complaintsEmail", encodeUtf8 <$> email), + ("complaintsPGP", encodeUtf8 . pkURI <$> pgp), + ("complaintsPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp) + ] + hostingE Entity {name, country} = + [ ("hosting", Just ""), + ("hostingEntity", Just $ encodeUtf8 name), + ("hostingCountry", encodeUtf8 <$> country) + ] + server = + [ ("serverCountry", fmap encodeUtf8 $ serverCountry =<< information) + ] + +-- Copy-pasted from simplex-chat Simplex.Chat.Types.Preferences +{-# INLINE timedTTLText #-} +timedTTLText :: (Integral i, Show i) => i -> String +timedTTLText 0 = "0 sec" +timedTTLText ttl = do + let (m', s) = ttl `quotRem` 60 + (h', m) = m' `quotRem` 60 + (d', h) = h' `quotRem` 24 + (mm, d) = d' `quotRem` 30 + unwords $ + [mms mm | mm /= 0] + <> [ds d | d /= 0] + <> [hs h | h /= 0] + <> [ms m | m /= 0] + <> [ss s | s /= 0] + where + ss s = show s <> " sec" + ms m = show m <> " min" + hs 1 = "1 hour" + hs h = show h <> " hours" + ds 1 = "1 day" + ds 7 = "1 week" + ds 14 = "2 weeks" + ds d = show d <> " days" + mms 1 = "1 month" + mms mm = show mm <> " months" + +-- | Rewrite source with provided substitutions +render :: ByteString -> [(ByteString, Maybe ByteString)] -> ByteString +render src = \case + [] -> src + (label, content') : rest -> render (section_ label content' src) rest + +-- | Rewrite section content inside @...@ markers. +-- Markers are always removed when found. Closing marker is mandatory. +-- If content is absent, whole section is removed. +-- Section content is delegated to `item_`. If no sections found, the whole source is delegated. +section_ :: ByteString -> Maybe ByteString -> ByteString -> ByteString +section_ label content' src = + case B.breakSubstring startMarker src of + (_, "") -> item_ label (fromMaybe "" content') src -- no section, just replace items + (before, afterStart') -> + -- found section start, search for end too + case B.breakSubstring endMarker $ B.drop (B.length startMarker) afterStart' of + (_, "") -> error $ "missing section end: " <> show endMarker + (inside, next') -> + let next = B.drop (B.length endMarker) next' + in case content' of + Nothing -> before <> next -- collapse section + Just content -> before <> item_ label content inside <> section_ label content' next + where + startMarker = " label <> ">" + endMarker = " label <> ">" + +-- | Replace all occurences of @${label}@ with provided content. +item_ :: ByteString -> ByteString -> ByteString -> ByteString +item_ label content' src = + case B.breakSubstring marker src of + (done, "") -> done + (before, after') -> before <> content' <> item_ label content' (B.drop (B.length marker) after') + where + marker = "${" <> label <> "}" diff --git a/apps/smp-server/web/Static/Embedded.hs b/apps/smp-server/web/Static/Embedded.hs new file mode 100644 index 000000000..23698dd6f --- /dev/null +++ b/apps/smp-server/web/Static/Embedded.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Static.Embedded where + +import Data.FileEmbed (embedDir, embedFile) +import Data.ByteString (ByteString) + +indexHtml :: ByteString +indexHtml = $(embedFile "apps/smp-server/static/index.html") + +linkHtml :: ByteString +linkHtml = $(embedFile "apps/smp-server/static/link.html") + +mediaContent :: [(FilePath, ByteString)] +mediaContent = $(embedDir "apps/smp-server/static/media/") diff --git a/package.yaml b/package.yaml index 02095d95f..68b93c1ac 100644 --- a/package.yaml +++ b/package.yaml @@ -22,6 +22,8 @@ extra-source-files: - CHANGELOG.md - cbits/sha512.h - cbits/sntrup761.h + - apps/smp-server/static/*.html + - apps/smp-server/static/media/* dependencies: - aeson == 2.2.* @@ -110,10 +112,16 @@ library: executables: smp-server: - source-dirs: apps/smp-server + source-dirs: + - apps/smp-server + - apps/smp-server/web main: Main.hs dependencies: + - file-embed - simplexmq + - wai-app-static + - warp + - warp-tls ghc-options: - -threaded - -rtsopts diff --git a/rfcs/2024-03-20-server-metadata.md b/rfcs/2024-03-20-server-metadata.md index f8795f130..22b163c05 100644 --- a/rfcs/2024-03-20-server-metadata.md +++ b/rfcs/2024-03-20-server-metadata.md @@ -52,8 +52,8 @@ source_code: https://github.com/simplex-chat/simplexmq # We should split this document to the model one, where specific parameters will be external to the document, # and specific to us, so that relay operators can adopt our recommended policy and publish any amendments separately. -conditions: https://github.com/simplex-chat/simplex-chat/blob/_archived-ep/ios-file-provider/PRIVACY.md -# conditions_amendments: link +usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/_archived-ep/ios-file-provider/PRIVACY.md +# condition_amendments: link server_country: SE operator: SimpleX Chat Ltd. @@ -62,9 +62,9 @@ website: https://simplex.chat admin_simplex: administrative SimpleX address admin_email: chat@simplex.chat admin_pgp: PGP key -feedback_simplex: SimpleX address for feedback, comments and complaints -feedback_email: complaints@simplex.chat -feedback_pgp: PGP key +complaints_simplex: SimpleX address for feedback, comments and complaints +complaints_email: complaints@simplex.chat +complaints_pgp: PGP key hosting: Linode / Akamai Inc. hosting_country: US ``` @@ -89,20 +89,27 @@ data ServerHandshake = ServerHandshake } data ServerInformation = ServerInformation - { -- below is based on the existing server configuration - persistence :: SMPServerPersistenceMode, + { config :: ServerPublicConfig, + info :: ServerPublicInfo + } + +-- based on server configuration +data ServerPublicConfig = ServerPublicConfig + { persistence :: SMPServerPersistenceMode, messageExpiration :: Int, statsEnabled :: Bool, newQueuesAllowed :: Bool, - basicAuthEnabled :: Bool, -- server is private if enabled - -- below is based on INFORMATION section of INI file - sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license - -- all below properties are optional, except entity name MUST be present if any entity country is present + basicAuthEnabled :: Bool -- server is private if enabled + } + +-- based on INFORMATION section of INI file +data ServerPublicInfo = ServerPublicInfo + { sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license conditions :: Maybe ServerConditions, operator :: Maybe Entity, website :: Maybe Text, - admin :: Maybe ServerContactAddress, - feedback :: Maybe ServerContactAddress, + adminContacts :: Maybe ServerContactAddress, + complaintsContacts :: Maybe ServerContactAddress, hosting :: Maybe Entity, serverCountry :: Maybe Text } diff --git a/simplexmq.cabal b/simplexmq.cabal index 890ecf1e8..7f53a2ba1 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -28,6 +28,33 @@ extra-source-files: CHANGELOG.md cbits/sha512.h cbits/sntrup761.h + apps/smp-server/static/index.html + apps/smp-server/static/link.html + apps/smp-server/static/media/apk_icon.png + apps/smp-server/static/media/apple_store.svg + apps/smp-server/static/media/contact.js + apps/smp-server/static/media/contact_page_mobile.png + apps/smp-server/static/media/f_droid.svg + apps/smp-server/static/media/favicon.ico + apps/smp-server/static/media/GilroyBold.woff2 + apps/smp-server/static/media/GilroyLight.woff2 + apps/smp-server/static/media/GilroyMedium.woff2 + apps/smp-server/static/media/GilroyRegular.woff2 + apps/smp-server/static/media/GilroyRegularItalic.woff2 + apps/smp-server/static/media/google_play.svg + apps/smp-server/static/media/logo-dark.png + apps/smp-server/static/media/logo-light.png + apps/smp-server/static/media/logo-symbol-dark.svg + apps/smp-server/static/media/logo-symbol-light.svg + apps/smp-server/static/media/moon.svg + apps/smp-server/static/media/qrcode.js + apps/smp-server/static/media/script.js + apps/smp-server/static/media/style.css + apps/smp-server/static/media/sun.svg + apps/smp-server/static/media/swiper-bundle.min.css + apps/smp-server/static/media/swiper-bundle.min.js + apps/smp-server/static/media/tailwind.css + apps/smp-server/static/media/testflight.png flag swift description: Enable swift JSON format @@ -140,6 +167,7 @@ library Simplex.Messaging.Server.Control Simplex.Messaging.Server.Env.STM Simplex.Messaging.Server.Expiration + Simplex.Messaging.Server.Information Simplex.Messaging.Server.Main Simplex.Messaging.Server.MsgStore Simplex.Messaging.Server.MsgStore.STM @@ -401,9 +429,12 @@ executable smp-agent executable smp-server main-is: Main.hs other-modules: + Static + Static.Embedded Paths_simplexmq hs-source-dirs: apps/smp-server + apps/smp-server/web default-extensions: StrictData ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts @@ -428,6 +459,7 @@ executable smp-server , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* + , file-embed , filepath ==1.4.* , hourglass ==0.2.* , http-types ==0.12.* @@ -456,6 +488,9 @@ executable smp-server , transformers ==0.6.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , wai-app-static + , warp + , warp-tls , websockets ==0.12.* , yaml ==0.11.* , zstd ==0.1.3.* diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index b909b1617..ee9a52d34 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -7,13 +7,13 @@ module Simplex.FileTransfer.Server.Main where -import qualified Data.ByteString.Char8 as B import Data.Either (fromRight) import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Int (Int64) import Data.Maybe (fromMaybe) import qualified Data.Text as T +import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative import Simplex.FileTransfer.Chunks @@ -29,6 +29,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) @@ -69,7 +70,7 @@ xftpServerCLI cfgPath logPath = do fp <- createServerX509 cfgPath x509cfg let host = fromMaybe (if ip == "127.0.0.1" then "" else ip) fqdn srv = ProtoServerWithAuth (XFTPServer [THDomainName host] "" (C.KeyHash fp)) Nothing - writeFile iniFile $ iniFileContent host + T.writeFile iniFile $ iniFileContent host putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server." warnCAPrivateKeyFile cfgPath x509cfg printServiceInfo serverVersion srv @@ -83,7 +84,7 @@ xftpServerCLI cfgPath logPath = do \# Log is compacted on start (deleted objects are removed).\n" <> ("enable: " <> onOff enableStoreLog <> "\n\n") <> "# Expire files after the specified number of hours.\n" - <> ("expire_files_hours: " <> show defFileExpirationHours <> "\n\n") + <> ("expire_files_hours: " <> tshow defFileExpirationHours <> "\n\n") <> "log_stats: off\n\ \\n\ \[AUTH]\n\ @@ -102,20 +103,20 @@ xftpServerCLI cfgPath logPath = do \# control_port_user_password:\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" - <> ("host: " <> host <> "\n") - <> ("port: " <> defaultServerPort <> "\n") + <> ("host: " <> T.pack host <> "\n") + <> ("port: " <> T.pack defaultServerPort <> "\n") <> "log_tls_errors: off\n\ \# control_port: 5226\n\ \\n\ \[FILES]\n" - <> ("path: " <> filesPath <> "\n") - <> ("storage_quota: " <> B.unpack (strEncode fileSizeQuota) <> "\n") + <> ("path: " <> T.pack filesPath <> "\n") + <> ("storage_quota: " <> safeDecodeUtf8 (strEncode fileSizeQuota) <> "\n") <> "\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" - <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") - <> ("# check_interval: " <> show (checkInterval defaultInactiveClientExpiration) <> "\n") + <> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n") + <> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n") runServer ini = do hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 0b40edee9..e44c2c0f7 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -108,6 +108,7 @@ module Simplex.Messaging.Agent.Protocol CRClientData, ServiceScheme, simplexChat, + connReqUriP', AgentErrorType (..), CommandErrorType (..), ConnectionErrorType (..), @@ -1161,7 +1162,7 @@ instance StrEncoding MsgReceipt where msgRcptStatus <- strP pure MsgReceipt {agentMsgId, msgRcptStatus} -instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where +instance ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where strEncode = \case CRInvitationUri crData e2eParams -> crEncode "invitation" crData (Just e2eParams) CRContactUri crData -> crEncode "contact" crData Nothing @@ -1175,34 +1176,40 @@ instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) whe [("v", strEncode crAgentVRange), ("smp", strEncode crSmpQueues)] <> maybe [] (\e2e -> [("e2e", strEncode e2e)]) e2eParams <> maybe [] (\cd -> [("data", encodeUtf8 cd)]) crClientData - strP = do - ACR m cr <- strP - case testEquality m $ sConnectionMode @m of - Just Refl -> pure cr - _ -> fail "bad connection request mode" + strP = connReqUriP' (Just SSSimplex) + +connReqUriP' :: forall m. ConnectionModeI m => Maybe ServiceScheme -> Parser (ConnectionRequestUri m) +connReqUriP' overrideScheme = do + ACR m cr <- connReqUriP overrideScheme + case testEquality m $ sConnectionMode @m of + Just Refl -> pure cr + _ -> fail "bad connection request mode" instance StrEncoding AConnectionRequestUri where strEncode (ACR _ cr) = strEncode cr - strP = do - _crScheme :: ServiceScheme <- strP - crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?" - query <- strP - aVRange <- queryParam "v" query - crSmpQueues <- queryParam "smp" query - let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query - let crData = ConnReqUriData {crScheme = SSSimplex, crAgentVRange = aVRange, crSmpQueues, crClientData} - case crMode of - CMInvitation -> do - crE2eParams <- queryParam "e2e" query - pure . ACR SCMInvitation $ CRInvitationUri crData crE2eParams - -- contact links are adjusted to the minimum version supported by the agent - -- to preserve compatibility with the old links published online - CMContact -> pure . ACR SCMContact $ CRContactUri crData {crAgentVRange = adjustAgentVRange aVRange} - where - crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact - adjustAgentVRange vr = - let v = max duplexHandshakeSMPAgentVersion $ minVersion vr - in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) + strP = connReqUriP (Just SSSimplex) + +connReqUriP :: Maybe ServiceScheme -> Parser AConnectionRequestUri +connReqUriP overrideScheme = do + crScheme <- (`fromMaybe` overrideScheme) <$> strP + crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?" + query <- strP + aVRange <- queryParam "v" query + crSmpQueues <- queryParam "smp" query + let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query + crData = ConnReqUriData {crScheme, crAgentVRange = aVRange, crSmpQueues, crClientData} + case crMode of + CMInvitation -> do + crE2eParams <- queryParam "e2e" query + pure . ACR SCMInvitation $ CRInvitationUri crData crE2eParams + -- contact links are adjusted to the minimum version supported by the agent + -- to preserve compatibility with the old links published online + CMContact -> pure . ACR SCMContact $ CRContactUri crData {crAgentVRange = adjustAgentVRange aVRange} + where + crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact + adjustAgentVRange vr = + let v = max duplexHandshakeSMPAgentVersion $ minVersion vr + in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) instance ConnectionModeI m => FromJSON (ConnectionRequestUri m) where parseJSON = strParseJSON "ConnectionRequestUri" diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index 2d2f0bfc4..351fe6d72 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -11,6 +11,7 @@ import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Maybe (fromMaybe) import qualified Data.Text as T +import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) @@ -25,6 +26,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) +import Simplex.Messaging.Util (tshow) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) @@ -65,7 +67,7 @@ ntfServerCLI cfgPath logPath = fp <- createServerX509 cfgPath x509cfg let host = fromMaybe (if ip == "127.0.0.1" then "" else ip) fqdn srv = ProtoServerWithAuth (NtfServer [THDomainName host] "" (C.KeyHash fp)) Nothing - writeFile iniFile $ iniFileContent host + T.writeFile iniFile $ iniFileContent host putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server." warnCAPrivateKeyFile cfgPath x509cfg printServiceInfo serverVersion srv @@ -81,15 +83,15 @@ ntfServerCLI cfgPath logPath = <> "log_stats: off\n\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" - <> ("host: " <> host <> "\n") - <> ("port: " <> defaultServerPort <> "\n") + <> ("host: " <> T.pack host <> "\n") + <> ("port: " <> T.pack defaultServerPort <> "\n") <> "log_tls_errors: off\n" <> "websockets: off\n\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" - <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") - <> ("# check_interval: " <> show (checkInterval defaultInactiveClientExpiration) <> "\n") + <> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n") + <> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n") runServer ini = do hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering diff --git a/src/Simplex/Messaging/Server/CLI.hs b/src/Simplex/Messaging/Server/CLI.hs index 9531a2ca5..956b30816 100644 --- a/src/Simplex/Messaging/Server/CLI.hs +++ b/src/Simplex/Messaging/Server/CLI.hs @@ -254,7 +254,7 @@ onOffPrompt prompt def = "N" -> pure False _ -> putStrLn "Invalid input, please enter 'y' or 'n'" >> onOffPrompt prompt def -onOff :: Bool -> String +onOff :: Bool -> Text onOff True = "on" onOff _ = "off" diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index f602c890b..4217ea9b9 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StrictData #-} module Simplex.Messaging.Server.Env.STM where @@ -15,6 +16,7 @@ import qualified Data.IntMap.Strict as IM import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Maybe (isJust, isNothing) import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) import Data.X509.Validation (Fingerprint (..)) @@ -27,6 +29,7 @@ import Simplex.Messaging.Crypto (KeyHash (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Expiration +import Simplex.Messaging.Server.Information import Simplex.Messaging.Server.MsgStore.STM import Simplex.Messaging.Server.QueueStore (NtfCreds (..), QueueRec (..)) import Simplex.Messaging.Server.QueueStore.STM @@ -81,9 +84,12 @@ data ServerConfig = ServerConfig transportConfig :: TransportServerConfig, -- | run listener on control port controlPort :: Maybe ServiceName, + -- | SMP proxy config smpAgentCfg :: SMPClientAgentConfig, allowSMPProxy :: Bool, -- auth is the same with `newQueueBasicAuth` - serverClientConcurrency :: Int + serverClientConcurrency :: Int, + -- | server public information + information :: Maybe ServerPublicInfo } defMsgExpirationDays :: Int64 @@ -108,6 +114,7 @@ defaultProxyClientConcurrency = 32 data Env = Env { config :: ServerConfig, + serverInfo :: ServerInformation, server :: Server, serverIdentity :: KeyHash, queueStore :: QueueStore, @@ -192,7 +199,7 @@ newSubscription subThread = do return Sub {subThread, delivered} newEnv :: ServerConfig -> IO Env -newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg, transportConfig} = do +newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg, transportConfig, information, messageExpiration} = do server <- atomically newServer queueStore <- atomically newQueueStore msgStore <- atomically newMsgStore @@ -206,7 +213,7 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, clientSeq <- newTVarIO 0 clients <- newTVarIO mempty proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random - return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent} + pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent} where restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do @@ -222,6 +229,23 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, addNotifier q = case notifier q of Nothing -> id Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q) + serverInfo = + ServerInformation + { information, + config = + ServerPublicConfig + { persistence, + messageExpiration = ttl <$> messageExpiration, + statsEnabled = isJust $ logStatsInterval config, + newQueuesAllowed = allowNewQueues config, + basicAuthEnabled = isJust $ newQueueBasicAuth config + } + } + where + persistence + | isNothing storeLogFile = SPMMemoryOnly + | isJust (storeMsgsFile config) = SPMMessages + | otherwise = SPMQueues newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent newSMPProxyAgent smpAgentCfg random = do diff --git a/src/Simplex/Messaging/Server/Information.hs b/src/Simplex/Messaging/Server/Information.hs new file mode 100644 index 000000000..01052541d --- /dev/null +++ b/src/Simplex/Messaging/Server/Information.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Messaging.Server.Information where + +import qualified Data.Aeson.TH as J +import Data.Int (Int64) +import Data.Text (Text) +import Simplex.Messaging.Agent.Protocol (ConnectionMode (..), ConnectionRequestUri) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) + +data ServerInformation = ServerInformation + { config :: ServerPublicConfig, + information :: Maybe ServerPublicInfo + } + deriving (Show) + +-- based on server configuration +data ServerPublicConfig = ServerPublicConfig + { persistence :: ServerPersistenceMode, + messageExpiration :: Maybe Int64, + statsEnabled :: Bool, + newQueuesAllowed :: Bool, + basicAuthEnabled :: Bool -- server is private if enabled + } + deriving (Show) + +-- based on INFORMATION section of INI file +data ServerPublicInfo = ServerPublicInfo + { sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license + usageConditions :: Maybe ServerConditions, + operator :: Maybe Entity, + website :: Maybe Text, + adminContacts :: Maybe ServerContactAddress, + complaintsContacts :: Maybe ServerContactAddress, + hosting :: Maybe Entity, + serverCountry :: Maybe Text + } + deriving (Show) + +data ServerPersistenceMode = SPMMemoryOnly | SPMQueues | SPMMessages + deriving (Show) + +data ServerConditions = ServerConditions {conditions :: Text, amendments :: Maybe Text} + deriving (Show) + +data Entity = Entity {name :: Text, country :: Maybe Text} + deriving (Show) + +data ServerContactAddress = ServerContactAddress + { simplex :: Maybe (ConnectionRequestUri 'CMContact), + email :: Maybe Text, -- it is recommended that it matches DNS email address, if either is present + pgp :: Maybe PGPKey + } + deriving (Show) + +data PGPKey = PGPKey {pkURI :: Text, pkFingerprint :: Text} + deriving (Show) + +$(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''ServerPersistenceMode) + +$(J.deriveJSON defaultJSON ''ServerConditions) + +$(J.deriveJSON defaultJSON ''Entity) + +$(J.deriveJSON defaultJSON ''PGPKey) + +$(J.deriveJSON defaultJSON ''ServerContactAddress) + +$(J.deriveJSON defaultJSON ''ServerPublicConfig) + +$(J.deriveJSON defaultJSON ''ServerPublicInfo) + +$(J.deriveJSON defaultJSON ''ServerInformation) diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 980a7b8d0..7af57ba25 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -5,34 +5,45 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Messaging.Server.Main where import Control.Concurrent.STM -import Control.Monad (void) +import Control.Logger.Simple +import Control.Monad (void, when, (<$!>)) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import Data.Char (isAlpha, isAscii, toUpper) import Data.Functor (($>)) -import Data.Ini (lookupValue, readIniFile) -import Data.Maybe (fromMaybe) +import Data.Ini (Ini, lookupValue, readIniFile) +import Data.List (find, isPrefixOf) +import qualified Data.List.NonEmpty as L +import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative +import Simplex.Messaging.Agent.Protocol (connReqUriP') import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig) import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer) import Simplex.Messaging.Server (runSMPServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration, defaultProxyClientConcurrency) import Simplex.Messaging.Server.Expiration +import Simplex.Messaging.Server.Information import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow) import Simplex.Messaging.Version (mkVersionRange) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) @@ -40,7 +51,10 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) import Text.Read (readMaybe) smpServerCLI :: FilePath -> FilePath -> IO () -smpServerCLI cfgPath logPath = +smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ()) + +smpServerCLI_ :: (ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) -> (EmbeddedWebParams -> IO ()) -> FilePath -> FilePath -> IO () +smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case Init opts -> doesFileExist iniFile >>= \case @@ -65,7 +79,10 @@ smpServerCLI cfgPath logPath = defaultServerPort = "5223" executableName = "smp-server" storeLogFilePath = combine logPath "smp-server-store.log" - initializeServer opts@InitOptions {ip, fqdn, scripted} + httpsCertFile = combine cfgPath "web.cert" + httpsKeyFile = combine cfgPath "web.key" + defaultStaticPath = combine logPath "www" + initializeServer opts@InitOptions {ip, fqdn, sourceCode = src', webStaticPath = sp', disableWeb = noWeb', scripted} | scripted = initialize opts | otherwise = do putStrLn "Use `smp-server init -h` for available options." @@ -76,7 +93,19 @@ smpServerCLI cfgPath logPath = password <- withPrompt "'r' for random (default), 'n' - no password, or enter password: " serverPassword let host = fromMaybe ip fqdn host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine - initialize opts {enableStoreLog, logStats, fqdn = if null host' then fqdn else Just host', password} + sourceCode' <- withPrompt ("Enter server source code URI (" <> maybe simplexmqSource T.unpack src' <> "): ") getServerSourceCode + staticPath' <- withPrompt ("Enter path to store generated static site with server information (" <> fromMaybe defaultStaticPath sp' <> "): ") getLine + enableWeb <- onOffPrompt "Enable built-in web server for static site" (not noWeb') + initialize + opts + { enableStoreLog, + logStats, + fqdn = if null host' then fqdn else Just host', + password, + sourceCode = (T.pack <$> sourceCode') <|> src', + webStaticPath = if null staticPath' then sp' else Just staticPath', + disableWeb = not enableWeb + } where serverPassword = getLine >>= \case @@ -87,7 +116,7 @@ smpServerCLI cfgPath logPath = case strDecode $ encodeUtf8 $ T.pack s of Right auth -> pure . Just $ ServerPassword auth _ -> putStrLn "Invalid password. Only latin letters, digits and symbols other than '@' and ':' are allowed" >> serverPassword - initialize InitOptions {enableStoreLog, logStats, signAlgorithm, password} = do + initialize InitOptions {enableStoreLog, logStats, signAlgorithm, password, sourceCode, webStaticPath, disableWeb} = do clearDirIfExists cfgPath clearDirIfExists logPath createDirectoryIfMissing True cfgPath @@ -97,26 +126,29 @@ smpServerCLI cfgPath logPath = basicAuth <- mapM createServerPassword password let host = fromMaybe (if ip == "127.0.0.1" then "" else ip) fqdn srv = ProtoServerWithAuth (SMPServer [THDomainName host] "" (C.KeyHash fp)) basicAuth - writeFile iniFile $ iniFileContent host basicAuth - putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server." + T.writeFile iniFile $ iniFileContent host basicAuth $ Just "https://github.com/simplex-chat/simplexmq" + putStrLn $ "Server initialized, please provide additional server information in " <> iniFile <> "." + putStrLn $ "Run `" <> executableName <> " start` to start server." warnCAPrivateKeyFile cfgPath x509cfg printServiceInfo serverVersion srv + printSourceCode sourceCode where createServerPassword = \case ServerPassword s -> pure s SPRandom -> BasicAuth . strEncode <$> (atomically . C.randomBytes 32 =<< C.newRandom) - iniFileContent host basicAuth = - "[STORE_LOG]\n\ - \# The server uses STM memory for persistence,\n\ - \# that will be lost on restart (e.g., as with redis).\n\ - \# This option enables saving memory to append only log,\n\ - \# and restoring it when the server is started.\n\ - \# Log is compacted on start (deleted objects are removed).\n" + iniFileContent host basicAuth sourceCode' = + informationIniContent sourceCode' + <> "[STORE_LOG]\n\ + \# The server uses STM memory for persistence,\n\ + \# that will be lost on restart (e.g., as with redis).\n\ + \# This option enables saving memory to append only log,\n\ + \# and restoring it when the server is started.\n\ + \# Log is compacted on start (deleted objects are removed).\n" <> ("enable: " <> onOff enableStoreLog <> "\n\n") <> "# Undelivered messages are optionally saved and restored when the server restarts,\n\ \# they are preserved in the .bak file until the next restart.\n" <> ("restore_messages: " <> onOff enableStoreLog <> "\n") - <> ("expire_messages_days: " <> show defMsgExpirationDays <> "\n\n") + <> ("expire_messages_days: " <> tshow defMsgExpirationDays <> "\n\n") <> "# Log daily server statistics to CSV file\n" <> ("log_stats: " <> onOff logStats <> "\n\n") <> "[AUTH]\n\ @@ -129,7 +161,7 @@ smpServerCLI cfgPath logPath = \# The password will not be shared with the connecting contacts, you must share it only\n\ \# with the users who you want to allow creating messaging queues on your server.\n" <> ( case basicAuth of - Just auth -> "create_password: " <> T.unpack (safeDecodeUtf8 $ strEncode auth) + Just auth -> "create_password: " <> safeDecodeUtf8 (strEncode auth) _ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')" ) <> "\n\n\ @@ -137,8 +169,8 @@ smpServerCLI cfgPath logPath = \# control_port_user_password:\n\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" - <> ("host: " <> host <> "\n") - <> ("port: " <> defaultServerPort <> "\n") + <> ("host: " <> T.pack host <> "\n") + <> ("port: " <> T.pack defaultServerPort <> "\n") <> "log_tls_errors: off\n\ \websockets: off\n\ \# control_port: 5224\n\n\ @@ -157,21 +189,36 @@ smpServerCLI cfgPath logPath = \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ \# socks_mode: onion\n\n\ \# Limit number of threads a client can spawn to process proxy commands in parrallel.\n" - <> ("# client_concurrency: " <> show defaultProxyClientConcurrency <> "\n\n") + <> ("# client_concurrency: " <> tshow defaultProxyClientConcurrency <> "\n\n") <> "[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" - <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") - <> ("# check_interval: " <> show (checkInterval defaultInactiveClientExpiration) <> "\n") + <> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n") + <> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n") + <> "\n\n\ + \[WEB]\n\ + \# Set path to generate static mini-site for server information and qr codes/links\n" + <> ("static_path: " <> T.pack (fromMaybe defaultStaticPath webStaticPath) <> "\n\n") + <> "# Run an embedded server on this port\n\ + \# Onion sites can use any port and register it in the hidden service config.\n\ + \# Running on a port 80 may require setting process capabilities.\n" + <> ((if disableWeb then "# " else "") <> "http: 8000\n\n") + <> "# You can run an embedded TLS web server too if you provide port and cert and key files.\n\ + \# Not required for running relay on onion address.\n\ + \# https: 443\n" + <> ("# cert: " <> T.pack httpsCertFile <> "\n") + <> ("# key: " <> T.pack httpsKeyFile <> "\n") runServer ini = do hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering fp <- checkSavedFingerprint cfgPath defaultX509Config let host = either (const "") T.unpack $ lookupValue "TRANSPORT" "host" ini port = T.unpack $ strictIni "TRANSPORT" "port" ini - cfg@ServerConfig {transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig + cfg@ServerConfig {information, transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig + sourceCode' = (\ServerPublicInfo {sourceCode} -> sourceCode) <$> information srv = ProtoServerWithAuth (SMPServer [THDomainName host] (if port == "5223" then "" else port) (C.KeyHash fp)) newQueueBasicAuth printServiceInfo serverVersion srv + printSourceCode sourceCode' printServerConfig transports storeLogFile putStrLn $ case messageExpiration of Just ExpirationConfig {ttl} -> "expiring messages after " <> showTTL ttl @@ -184,6 +231,20 @@ smpServerCLI cfgPath logPath = <> if allowNewQueues cfg then maybe "allowed" (const "requires password") newQueueBasicAuth else "NOT allowed" + -- print information + let persistence + | isNothing storeLogFile = SPMMemoryOnly + | isJust (storeMsgsFile cfg) = SPMMessages + | otherwise = SPMQueues + let config = + ServerPublicConfig + { persistence, + messageExpiration = ttl <$> messageExpiration, + statsEnabled = isJust logStats, + newQueuesAllowed = allowNewQueues cfg, + basicAuthEnabled = isJust newQueueBasicAuth + } + runWebServer ini ServerInformation {config, information} runSMPServer cfg where enableStoreLog = settingIsOn "STORE_LOG" "enable" ini @@ -211,9 +272,9 @@ smpServerCLI cfgPath logPath = _ -> enableStoreLog $> messagesPath, -- allow creating new queues by default allowNewQueues = fromMaybe True $ iniOnOff "AUTH" "new_queues" ini, - newQueueBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini, - controlPortAdminAuth = either error id <$> strDecodeIni "AUTH" "control_port_admin_password" ini, - controlPortUserAuth = either error id <$> strDecodeIni "AUTH" "control_port_user_password" ini, + newQueueBasicAuth = either error id <$!> strDecodeIni "AUTH" "create_password" ini, + controlPortAdminAuth = either error id <$!> strDecodeIni "AUTH" "control_port_admin_password" ini, + controlPortUserAuth = either error id <$!> strDecodeIni "AUTH" "control_port_user_password" ini, messageExpiration = Just defaultMessageExpiration @@ -235,7 +296,7 @@ smpServerCLI cfgPath logPath = { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, alpn = Just supportedSMPHandshakes }, - controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini, + controlPort = eitherToMaybe $ T.unpack <$> lookupValue "TRANSPORT" "control_port" ini, smpAgentCfg = defaultSMPClientAgentConfig { smpCfg = @@ -244,7 +305,7 @@ smpServerCLI cfgPath logPath = agreeSecret = True, networkConfig = defaultNetworkConfig - { socksProxy = either error id <$> strDecodeIni "PROXY" "socks_proxy" ini, + { socksProxy = either error id <$!> strDecodeIni "PROXY" "socks_proxy" ini, socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini, hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini, requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini @@ -254,7 +315,8 @@ smpServerCLI cfgPath logPath = persistErrorInterval = 30 -- seconds }, allowSMPProxy = True, - serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini + serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini, + information = serverPublicInfo ini } textToSocksMode :: Text -> SocksMode textToSocksMode = \case @@ -269,6 +331,126 @@ smpServerCLI cfgPath logPath = textToOwnServers :: Text -> [ByteString] textToOwnServers = map encodeUtf8 . T.words + runWebServer ini si = + case eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini of + Nothing -> logWarn "No server static path set" + Just webStaticPath -> do + let onionHost = + either (const Nothing) (find isOnion) $ + strDecode @(L.NonEmpty TransportHost) . encodeUtf8 =<< lookupValue "TRANSPORT" "host" ini + webHttpPort = eitherToMaybe $ read . T.unpack <$> lookupValue "WEB" "http" ini + webHttpsParams = + eitherToMaybe $ do + port <- read . T.unpack <$> lookupValue "WEB" "https" ini + cert <- T.unpack <$> lookupValue "WEB" "cert" ini + key <- T.unpack <$> lookupValue "WEB" "key" ini + pure WebHttpsParams {port, cert, key} + generateSite si onionHost webStaticPath + when (isJust webHttpPort || isJust webHttpsParams) $ + serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} + where + isOnion = \case THOnionHost _ -> True; _ -> False + +data EmbeddedWebParams = EmbeddedWebParams + { webStaticPath :: FilePath, + webHttpPort :: Maybe Int, + webHttpsParams :: Maybe WebHttpsParams + } + +data WebHttpsParams = WebHttpsParams + { port :: Int, + cert :: FilePath, + key :: FilePath + } + +getServerSourceCode :: IO (Maybe String) +getServerSourceCode = + getLine >>= \case + "" -> pure Nothing + s | "https://" `isPrefixOf` s || "http://" `isPrefixOf` s -> pure $ Just s + _ -> putStrLn "Invalid source code. URI should start from http:// or https://" >> getServerSourceCode + +simplexmqSource :: String +simplexmqSource = "https://github.com/simplex-chat/simplexmq" + +informationIniContent :: Maybe Text -> Text +informationIniContent sourceCode_ = + "[INFORMATION]\n\ + \# AGPLv3 license requires that you make any source code modifications\n\ + \# available to the end users of the server.\n\ + \# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE\n\ + \# Include correct source code URI in case the server source code is modified in any way.\n\ + \# If any other information fields are present, source code property also MUST be present.\n\n" + <> (maybe "# source_code: URI" ("source_code: " <>) sourceCode_ <> "\n\n") + <> "# Declaring all below information is optional, any of these fields can be omitted.\n\ + \\n\ + \# Server usage conditions and amendments.\n\ + \# It is recommended to use standard conditions with any amendments in a separate document.\n\ + \# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md\n\ + \# condition_amendments: link\n\ + \\n\ + \# Server location and operator.\n\ + \# server_country: ISO-3166 2-letter code\n\ + \# operator: entity (organization or person name)\n\ + \# operator_country: ISO-3166 2-letter code\n\ + \# website:\n\ + \\n\ + \# Administrative contacts.\n\ + \# admin_simplex: SimpleX address\n\ + \# admin_email:\n\ + \# admin_pgp:\n\ + \# admin_pgp_fingerprint:\n\ + \\n\ + \# Contacts for complaints and feedback.\n\ + \# complaints_simplex: SimpleX address\n\ + \# complaints_email:\n\ + \# complaints_pgp:\n\ + \# complaints_pgp_fingerprint:\n\ + \\n\ + \# Hosting provider.\n\ + \# hosting: entity (organization or person name)\n\ + \# hosting_country: ISO-3166 2-letter code\n\n" + +serverPublicInfo :: Ini -> Maybe ServerPublicInfo +serverPublicInfo ini = serverInfo <$!> infoValue "source_code" + where + serverInfo sourceCode = + ServerPublicInfo + { sourceCode, + usageConditions = + (\conditions -> ServerConditions {conditions, amendments = infoValue "condition_amendments"}) + <$!> infoValue "usage_conditions", + serverCountry = countryValue "server_country", + operator = iniEntity "operator" "operator_country", + website = infoValue "website", + adminContacts = iniContacts "admin_simplex" "admin_email" "admin_pgp" "admin_pgp_fingerprint", + complaintsContacts = iniContacts "complaints_simplex" "complaints_email" "complaints_pgp" "complaints_pgp_fingerprint", + hosting = iniEntity "hosting" "hosting_country" + } + infoValue name = eitherToMaybe $ lookupValue "INFORMATION" name ini + iniEntity nameField countryField = + (\name -> Entity {name, country = countryValue countryField}) + <$!> infoValue nameField + countryValue field = + (\cs -> if T.length cs == 2 && T.all (\c -> isAscii c && isAlpha c) cs then T.map toUpper cs else error $ "Use ISO3166 2-letter code for " <> T.unpack field) + <$!> infoValue field + iniContacts simplexField emailField pgpKeyUriField pgpKeyFingerprintField = + let simplex = either error id . parseAll (connReqUriP' Nothing) . encodeUtf8 <$!> eitherToMaybe (lookupValue "INFORMATION" simplexField ini) + email = infoValue emailField + pkURI_ = infoValue pgpKeyUriField + pkFingerprint_ = infoValue pgpKeyFingerprintField + in case (simplex, email, pkURI_, pkFingerprint_) of + (Nothing, Nothing, Nothing, _) -> Nothing + (Nothing, Nothing, _, Nothing) -> Nothing + (_, _, pkURI, pkFingerprint) -> Just ServerContactAddress {simplex, email, pgp = PGPKey <$> pkURI <*> pkFingerprint} + +printSourceCode :: Maybe Text -> IO () +printSourceCode = \case + Just sourceCode -> T.putStrLn $ "Server source code: " <> sourceCode + Nothing -> do + putStrLn "Warning: server source code is not specified." + putStrLn "Add 'source_code' property to [INFORMATION] section of INI file." + data CliCommand = Init InitOptions | OnlineCert CertOptions @@ -282,6 +464,9 @@ data InitOptions = InitOptions ip :: HostName, fqdn :: Maybe HostName, password :: Maybe ServerPassword, + sourceCode :: Maybe Text, + webStaticPath :: Maybe FilePath, + disableWeb :: Bool, scripted :: Bool } deriving (Show) @@ -336,7 +521,6 @@ cliCommandP cfgPath logPath iniFile = ( long "fqdn" <> short 'n' <> help "Server FQDN used as Common Name for TLS online certificate" - <> showDefault <> metavar "FQDN" ) password <- @@ -349,12 +533,41 @@ cliCommandP cfgPath logPath iniFile = <> help "Set password to create new messaging queues" <> value SPRandom ) + sourceCode <- + (optional . strOption) + ( long "source-code" + <> help "Server source code will be communicated to the users" + <> metavar "URI" + ) + webStaticPath <- + (optional . strOption) + ( long "web-path" + <> help "Directory to store generated static site with server information" + <> metavar "PATH" + ) + disableWeb <- + switch + ( long "disable-web" + <> help "Disable starting static web server with server information" + ) scripted <- switch ( long "yes" <> short 'y' <> help "Non-interactive initialization using command-line options" ) - pure InitOptions {enableStoreLog, logStats, signAlgorithm, ip, fqdn, password, scripted} + pure + InitOptions + { enableStoreLog, + logStats, + signAlgorithm, + ip, + fqdn, + password, + sourceCode, + webStaticPath, + disableWeb, + scripted + } parseBasicAuth :: ReadM ServerPassword parseBasicAuth = eitherReader $ fmap ServerPassword . strDecode . B.pack diff --git a/tests/CLITests.hs b/tests/CLITests.hs index 3bc48c5ce..2193de5e9 100644 --- a/tests/CLITests.hs +++ b/tests/CLITests.hs @@ -57,7 +57,7 @@ smpServerTest :: Bool -> Bool -> IO () smpServerTest storeLog basicAuth = do -- init capture_ (withArgs (["init", "-y"] <> ["-l" | storeLog] <> ["--no-password" | not basicAuth]) $ smpServerCLI cfgPath logPath) - >>= (`shouldSatisfy` (("Server initialized, you can modify configuration in " <> cfgPath <> "/smp-server.ini") `isPrefixOf`)) + >>= (`shouldSatisfy` (("Server initialized, please provide additional server information in " <> cfgPath <> "/smp-server.ini") `isPrefixOf`)) Right ini <- readIniFile $ cfgPath <> "/smp-server.ini" lookupValue "STORE_LOG" "enable" ini `shouldBe` Right (if storeLog then "on" else "off") lookupValue "STORE_LOG" "log_stats" ini `shouldBe` Right "off" diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index f8c0e22c1..6bc36c29a 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -123,7 +123,8 @@ cfg = controlPort = Nothing, smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 1}, -- seconds allowSMPProxy = False, - serverClientConcurrency = 2 + serverClientConcurrency = 2, + information = Nothing } cfgV7 :: ServerConfig From 15f0bb9e7913f1f1ee037598c0367773c5a06942 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Wed, 29 May 2024 15:18:00 +0300 Subject: [PATCH 076/125] tcp-server: recover from accept errors (#1179) * tcp-server: recover from accept errors * log * warn * where * retry --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Messaging/Transport/Server.hs | 28 +++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Transport/Server.hs b/src/Simplex/Messaging/Transport/Server.hs index 495ad76bb..ffde39991 100644 --- a/src/Simplex/Messaging/Transport/Server.hs +++ b/src/Simplex/Messaging/Transport/Server.hs @@ -19,7 +19,7 @@ module Simplex.Messaging.Transport.Server loadTLSServerParams, loadFingerprint, smpServerHandshake, - tlsServerCredentials + tlsServerCredentials, ) where @@ -35,11 +35,14 @@ import Data.Maybe (fromJust, fromMaybe) import qualified Data.X509 as X import Data.X509.Validation (Fingerprint (..)) import qualified Data.X509.Validation as XV +import Foreign.C.Error +import GHC.IO.Exception (ioe_errno) import Network.Socket import qualified Network.TLS as T import Simplex.Messaging.Transport import Simplex.Messaging.Util (catchAll_, labelMyThread, tshow) import System.Exit (exitFailure) +import System.IO.Error (tryIOError) import System.Mem.Weak (Weak, deRefWeak) import UnliftIO (timeout) import UnliftIO.Concurrent @@ -113,7 +116,7 @@ runTCPServer started port server = do runTCPServerSocket :: SocketState -> TMVar Bool -> IO Socket -> (Socket -> IO ()) -> IO () runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket server = E.bracket getSocket (closeServer started clients) $ \sock -> - forever . E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do + forever . E.bracketOnError (safeAccept sock) (close . fst) $ \(conn, _peer) -> do cId <- atomically $ stateTVar accepted $ \cId -> let cId' = cId + 1 in cId `seq` (cId', cId') let closeConn _ = do atomically $ modifyTVar' clients $ IM.delete cId @@ -122,6 +125,27 @@ runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket serve tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn atomically $ modifyTVar' clients $ IM.insert cId tId +-- | Recover from errors in `accept` whenever it is safe. +-- Some errors are safe to ignore, while blindly restaring `accept` may trigger a busy loop. +-- +-- man accept says: +-- @ +-- For reliable operation the application should detect the network errors defined for the protocol after accept() and treat them like EAGAIN by retrying. +-- In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. +-- @ +safeAccept :: Socket -> IO (Socket, SockAddr) +safeAccept sock = + tryIOError (accept sock) >>= \case + Right r -> pure r + Left e + | retryAccept -> logWarn err >> safeAccept sock + | otherwise -> logError err >> E.throwIO e + where + retryAccept = maybe False ((`elem` again) . Errno) errno + again = [eAGAIN, eNETDOWN, ePROTO, eNOPROTOOPT, eHOSTDOWN, eNONET, eHOSTUNREACH, eOPNOTSUPP, eNETUNREACH] + err = "socket accept error: " <> tshow e <> maybe "" ((", errno=" <>) . tshow) errno + errno = ioe_errno e + type SocketState = (TVar Int, TVar Int, TVar (IntMap (Weak ThreadId))) newSocketState :: STM SocketState From 39b3b5a25ece03fa559a9a852543305c9c0767d3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 29 May 2024 13:19:10 +0100 Subject: [PATCH 077/125] 5.8.0.8 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 68b93c1ac..6bdb03e4d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.7 +version: 5.8.0.8 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 7f53a2ba1..829fd23df 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.7 +version: 5.8.0.8 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 97a953550f90b14dbf0f9a62077e88df306ebeca Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 30 May 2024 14:21:29 +0400 Subject: [PATCH 078/125] agent: getAgentQueuesInfo (#1180) --- src/Simplex/Messaging/Agent/Client.hs | 87 ++++++++++++++++++++------- src/Simplex/Messaging/Client.hs | 25 ++++++++ src/Simplex/Messaging/Client/Agent.hs | 16 ++--- src/Simplex/Messaging/Session.hs | 10 +-- 4 files changed, 106 insertions(+), 32 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index a99d957e3..bc2a41ae2 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -137,6 +137,8 @@ module Simplex.Messaging.Agent.Client getAgentWorkersDetails, AgentWorkersSummary (..), getAgentWorkersSummary, + AgentQueuesInfo (..), + getAgentQueuesInfo, SMPTransportSession, NtfTransportSession, XFTPTransportSession, @@ -204,7 +206,7 @@ import Simplex.Messaging.Notifications.Client import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Transport (NTFVersion) import Simplex.Messaging.Notifications.Types -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse, sumTypeJSON) import Simplex.Messaging.Protocol ( AProtocolType (..), BrokerMsg, @@ -582,7 +584,8 @@ instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do unlessM (readTVarIO active) . throwError $ INACTIVE - atomically (getSessVar workerSeq tSess smpClients) + ts <- liftIO getCurrentTime + atomically (getSessVar workerSeq tSess smpClients ts) >>= either newClient (waitForProtocolClient c tSess smpClients) where newClient v = do @@ -593,28 +596,30 @@ getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClien getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do unlessM (readTVarIO active) . throwError $ INACTIVE proxySrv <- getNextServer c userId [destSrv] - atomically (getClientVar proxySrv) >>= \(tSess, auth, v) -> - either (newProxyClient tSess auth) (waitForProxyClient tSess auth) v + ts <- liftIO getCurrentTime + atomically (getClientVar proxySrv ts) >>= \(tSess, auth, v) -> + either (newProxyClient tSess auth ts) (waitForProxyClient tSess auth) v where - getClientVar :: SMPServerWithAuth -> STM (SMPTransportSession, Maybe SMP.BasicAuth, Either SMPClientVar SMPClientVar) - getClientVar proxySrv = do + getClientVar :: SMPServerWithAuth -> UTCTime -> STM (SMPTransportSession, Maybe SMP.BasicAuth, Either SMPClientVar SMPClientVar) + getClientVar proxySrv ts = do ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure let tSess = (userId, srv, qId) - (tSess,auth,) <$> getSessVar workerSeq tSess smpClients - newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) - newProxyClient tSess auth v = do + (tSess,auth,) <$> getSessVar workerSeq tSess smpClients ts + newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> UTCTime -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) + newProxyClient tSess auth ts v = do (prs, rv) <- atomically $ do prs <- TM.empty -- we do not need to check if it is a new proxied relay session, -- as the client is just created and there are no sessions yet - (prs,) . either id id <$> getSessVar workerSeq destSrv prs + (prs,) . either id id <$> getSessVar workerSeq destSrv prs ts clnt <- smpConnectClient c tSess prs v (clnt,) <$> newProxiedRelay clnt auth rv waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) waitForProxyClient tSess auth v = do clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess smpClients v + ts <- liftIO getCurrentTime sess <- - atomically (getSessVar workerSeq destSrv prs) + atomically (getSessVar workerSeq destSrv prs ts) >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) pure (clnt, sess) newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) @@ -688,14 +693,15 @@ smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) resubscribeSMPSession :: AgentClient -> SMPTransportSession -> AM' () -resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = - atomically getWorkerVar >>= mapM_ (either newSubWorker (\_ -> pure ())) +resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = do + ts <- liftIO getCurrentTime + atomically (getWorkerVar ts) >>= mapM_ (either newSubWorker (\_ -> pure ())) where - getWorkerVar = + getWorkerVar ts = ifM (null <$> getPending) (pure Nothing) -- prevent race with cleanup and adding pending queues in another call - (Just <$> getSessVar workerSeq tSess smpSubWorkers) + (Just <$> getSessVar workerSeq tSess smpSubWorkers ts) newSubWorker v = do a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v) atomically $ putTMVar (sessionVar v) a @@ -740,7 +746,8 @@ reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, srv, _) = do unlessM (readTVarIO active) . throwError $ INACTIVE - atomically (getSessVar workerSeq tSess ntfClients) + ts <- liftIO getCurrentTime + atomically (getSessVar workerSeq tSess ntfClients ts) >>= either (newProtocolClient c tSess ntfClients connectClient) (waitForProtocolClient c tSess ntfClients) @@ -763,7 +770,8 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId, srv, _) = do unlessM (readTVarIO active) . throwError $ INACTIVE - atomically (getSessVar workerSeq tSess xftpClients) + ts <- liftIO getCurrentTime + atomically (getSessVar workerSeq tSess xftpClients ts) >>= either (newProtocolClient c tSess xftpClients connectClient) (waitForProtocolClient c tSess xftpClients) @@ -1060,10 +1068,11 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do $>>= \(ProtoServerWithAuth srv _) -> tryReadSessVar (userId, srv, qId) (smpClients c) ) >>= \case - Just (Right (SMPConnectedClient smp' prs)) | sameClient smp' -> - tryReadSessVar destSrv prs >>= \case - Just (Right proxySess') | sameProxiedRelay proxySess' -> TM.delete destSrv prs - _ -> pure () + Just (Right (SMPConnectedClient smp' prs)) + | sameClient smp' -> + tryReadSessVar destSrv prs >>= \case + Just (Right proxySess') | sameProxiedRelay proxySess' -> TM.delete destSrv prs + _ -> pure () _ -> pure () sameClient smp' = sessionId (thParams smp) == sessionId (thParams smp') sameProxiedRelay proxySess' = prSessionId proxySess == prSessionId proxySess' @@ -2041,6 +2050,38 @@ getAgentWorkersSummary AgentClient {smpClients, ntfClients, xftpClients, smpDeli (pure WorkersSummary {numActive, numIdle = numIdle + 1, totalRestarts = totalRestarts + restartCount}) (pure WorkersSummary {numActive = numActive + 1, numIdle, totalRestarts = totalRestarts + restartCount}) +data AgentQueuesInfo = AgentQueuesInfo + { msgQInfo :: TBQueueInfo, + subQInfo :: TBQueueInfo, + smpClientsQueues :: Map Text (Int, UTCTime, ClientInfo) + } + deriving (Show) + +data ClientInfo + = ClientInfoQueues {sndQInfo :: TBQueueInfo, rcvQInfo :: TBQueueInfo} + | ClientInfoError {clientError :: (AgentErrorType, Maybe UTCTime)} + | ClientInfoConnecting + deriving (Show) + +getAgentQueuesInfo :: AgentClient -> IO AgentQueuesInfo +getAgentQueuesInfo AgentClient {msgQ, subQ, smpClients} = do + msgQInfo <- atomically $ getTBQueueInfo msgQ + subQInfo <- atomically $ getTBQueueInfo subQ + smpClientsMap <- readTVarIO smpClients + let smpClientsMap' = M.mapKeys (decodeLatin1 . strEncode) smpClientsMap + smpClientsQueues <- mapM getClientQueuesInfo smpClientsMap' + pure AgentQueuesInfo {msgQInfo, subQInfo, smpClientsQueues} + where + getClientQueuesInfo :: SMPClientVar -> IO (Int, UTCTime, ClientInfo) + getClientQueuesInfo SessionVar {sessionVar, sessionVarId, sessionVarTs} = do + clientInfo <- atomically (tryReadTMVar sessionVar) >>= \case + Just (Right c) -> do + (sndQInfo, rcvQInfo) <- getProtocolClientQueuesInfo $ protocolClient c + pure ClientInfoQueues {sndQInfo, rcvQInfo} + Just (Left e) -> pure $ ClientInfoError e + Nothing -> pure ClientInfoConnecting + pure (sessionVarId, sessionVarTs, clientInfo) + $(J.deriveJSON defaultJSON ''AgentLocks) $(J.deriveJSON (enumJSON $ dropPrefix "TS") ''ProtocolTestStep) @@ -2059,6 +2100,10 @@ $(J.deriveJSON defaultJSON {J.fieldLabelModifier = takeWhile (/= '_')} ''AgentWo $(J.deriveJSON defaultJSON ''AgentWorkersSummary) +$(J.deriveJSON (sumTypeJSON $ dropPrefix "ClientInfo") ''ClientInfo) + +$(J.deriveJSON defaultJSON ''AgentQueuesInfo) + $(J.deriveJSON (enumJSON $ dropPrefix "UN") ''UserNetworkType) $(J.deriveJSON defaultJSON ''UserNetworkInfo) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index e33c91873..8c23db822 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -90,6 +90,11 @@ module Simplex.Messaging.Client mkTransmission, authTransmission, smpClientStub, + + -- * For debugging + TBQueueInfo (..), + getTBQueueInfo, + getProtocolClientQueuesInfo, ) where @@ -1054,6 +1059,24 @@ authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_ sign :: forall a. (C.AlgorithmI a, C.SignatureAlgorithm a) => C.PrivateKey a -> Either TransportError TransmissionAuth sign pk = Right $ TASignature $ C.ASignature (C.sAlgorithm @a) (C.sign' pk t) +data TBQueueInfo = TBQueueInfo + { qLength :: Int, + qFull :: Bool + } + deriving (Show) + +getTBQueueInfo :: TBQueue a -> STM TBQueueInfo +getTBQueueInfo q = do + qLength <- fromIntegral <$> lengthTBQueue q + qFull <- isFullTBQueue q + pure TBQueueInfo {qLength, qFull} + +getProtocolClientQueuesInfo :: ProtocolClient v err msg -> IO (TBQueueInfo, TBQueueInfo) +getProtocolClientQueuesInfo ProtocolClient {client_ = PClient {sndQ, rcvQ}} = do + sndQInfo <- atomically $ getTBQueueInfo sndQ + rcvQInfo <- atomically $ getTBQueueInfo rcvQ + pure (sndQInfo, rcvQInfo) + $(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode) $(J.deriveJSON (enumJSON $ dropPrefix "SM") ''SocksMode) @@ -1067,3 +1090,5 @@ $(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback) $(J.deriveJSON defaultJSON ''NetworkConfig) $(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) + +$(J.deriveJSON defaultJSON ''TBQueueInfo) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 3b4a78cf5..8781d87a6 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -143,10 +143,11 @@ getSMPServerClient' ca srv = snd <$> getSMPServerClient'' ca srv {-# INLINE getSMPServerClient' #-} getSMPServerClient'' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO (OwnServer, SMPClient) -getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv = - atomically getClientVar >>= either (ExceptT . newSMPClient) waitForSMPClient +getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv = do + ts <- liftIO getCurrentTime + atomically (getClientVar ts) >>= either (ExceptT . newSMPClient) waitForSMPClient where - getClientVar :: STM (Either SMPClientVar SMPClientVar) + getClientVar :: UTCTime -> STM (Either SMPClientVar SMPClientVar) getClientVar = getSessVar workerSeq srv smpClients waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO (OwnServer, SMPClient) @@ -227,14 +228,15 @@ connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, random -- | Spawn reconnect worker if needed reconnectClient :: SMPClientAgent -> SMPServer -> IO () -reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} srv = - whenM (readTVarIO active) $ atomically getWorkerVar >>= mapM_ (either newSubWorker (\_ -> pure ())) +reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} srv = do + ts <- getCurrentTime + whenM (readTVarIO active) $ atomically (getWorkerVar ts) >>= mapM_ (either newSubWorker (\_ -> pure ())) where - getWorkerVar = + getWorkerVar ts = ifM (null <$> getPending) (pure Nothing) -- prevent race with cleanup and adding pending queues in another call - (Just <$> getSessVar workerSeq srv smpSubWorkers) + (Just <$> getSessVar workerSeq srv smpSubWorkers ts) newSubWorker :: SessionVar (Async ()) -> IO () newSubWorker v = do a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v) diff --git a/src/Simplex/Messaging/Session.hs b/src/Simplex/Messaging/Session.hs index 75543b481..3ce5a35c8 100644 --- a/src/Simplex/Messaging/Session.hs +++ b/src/Simplex/Messaging/Session.hs @@ -8,23 +8,25 @@ import Control.Concurrent.STM import Control.Monad import Data.Composition ((.:.)) import Data.Functor (($>)) +import Data.Time (UTCTime) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (($>>=)) data SessionVar a = SessionVar { sessionVar :: TMVar a, - sessionVarId :: Int + sessionVarId :: Int, + sessionVarTs :: UTCTime } -getSessVar :: forall k a. Ord k => TVar Int -> k -> TMap k (SessionVar a) -> STM (Either (SessionVar a) (SessionVar a)) -getSessVar sessSeq sessKey vs = maybe (Left <$> newSessionVar) (pure . Right) =<< TM.lookup sessKey vs +getSessVar :: forall k a. Ord k => TVar Int -> k -> TMap k (SessionVar a) -> UTCTime -> STM (Either (SessionVar a) (SessionVar a)) +getSessVar sessSeq sessKey vs sessionVarTs = maybe (Left <$> newSessionVar) (pure . Right) =<< TM.lookup sessKey vs where newSessionVar :: STM (SessionVar a) newSessionVar = do sessionVar <- newEmptyTMVar sessionVarId <- stateTVar sessSeq $ \next -> (next, next + 1) - let v = SessionVar {sessionVar, sessionVarId} + let v = SessionVar {sessionVar, sessionVarId, sessionVarTs} TM.insert sessKey v vs pure v From 88f1b727e0e4bfef01a346d818b7f6d94a181b16 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 30 May 2024 18:49:43 +0100 Subject: [PATCH 079/125] SMP protocol extension to debug subscribed SMP queues (#1181) * SMP protocol extension to debug subscribed SMP queues * fix, test * corrections Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * increase delays * increase timeout * delay * delay * enable all tests --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- simplexmq.cabal | 1 + src/Simplex/Messaging/Agent.hs | 16 +++ src/Simplex/Messaging/Agent/Client.hs | 7 ++ src/Simplex/Messaging/Client.hs | 8 ++ src/Simplex/Messaging/Protocol.hs | 25 ++++- src/Simplex/Messaging/Server.hs | 24 ++++- .../Messaging/Server/QueueStore/QueueInfo.hs | 55 +++++++++++ src/Simplex/RemoteControl/Invitation.hs | 2 +- tests/AgentTests/FunctionalAPITests.hs | 98 ++++++++++++++++++- tests/CLITests.hs | 4 +- 10 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs diff --git a/simplexmq.cabal b/simplexmq.cabal index 829fd23df..31dfff436 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -172,6 +172,7 @@ library Simplex.Messaging.Server.MsgStore Simplex.Messaging.Server.MsgStore.STM Simplex.Messaging.Server.QueueStore + Simplex.Messaging.Server.QueueStore.QueueInfo Simplex.Messaging.Server.QueueStore.STM Simplex.Messaging.Server.Stats Simplex.Messaging.Server.StoreLog diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 76d7d333d..073660ba0 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -70,6 +70,7 @@ module Simplex.Messaging.Agent sendMessages, sendMessagesB, ackMessage, + getConnectionQueueInfo, switchConnection, abortConnectionSwitch, synchronizeRatchet, @@ -176,6 +177,7 @@ import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (SMPVersion, THandleParams (sessionId)) @@ -371,6 +373,10 @@ ackMessage :: AgentClient -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AE ackMessage c = withAgentEnv c .:. ackMessage' c {-# INLINE ackMessage #-} +getConnectionQueueInfo :: AgentClient -> ConnId -> AE QueueInfo +getConnectionQueueInfo c = withAgentEnv c . getConnectionQueueInfo' c +{-# INLINE getConnectionQueueInfo #-} + -- | Switch connection to the new receive queue switchConnection :: AgentClient -> ConnId -> AE ConnectionStats switchConnection c = withAgentEnv c . switchConnection' c @@ -1510,6 +1516,16 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do withStore' c $ \db -> deleteDeliveredSndMsg db connId $ InternalId sndMsgId _ -> pure () +getConnectionQueueInfo' :: AgentClient -> ConnId -> AM QueueInfo +getConnectionQueueInfo' c connId = do + SomeConn _ conn <- withStore c (`getConn` connId) + case conn of + DuplexConnection _ (rq :| _) _ -> getQueueInfo c rq + RcvConnection _ rq -> getQueueInfo c rq + ContactConnection _ rq -> getQueueInfo c rq + SndConnection {} -> throwE $ CONN SIMPLEX + NewConnection _ -> throwE $ CMD PROHIBITED "getConnectionQueueInfo': NewConnection" + switchConnection' :: AgentClient -> ConnId -> AM ConnectionStats switchConnection' c connId = withConnLock c connId "switchConnection" $ diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index bc2a41ae2..e63ab5d1d 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -56,6 +56,7 @@ module Simplex.Messaging.Agent.Client disableQueueNotifications, disableQueuesNtfs, sendAgentMessage, + getQueueInfo, agentNtfRegisterToken, agentNtfVerifyToken, agentNtfCheckToken, @@ -238,6 +239,7 @@ import Simplex.Messaging.Protocol sameSrvAddr', ) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM @@ -1577,6 +1579,11 @@ sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags a msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg sendOrProxySMPMessage c userId server "" (Just sndPrivateKey) sndId msgFlags msg +getQueueInfo :: AgentClient -> RcvQueue -> AM QueueInfo +getQueueInfo c rq@RcvQueue {rcvId, rcvPrivateKey} = + withSMPClient c rq "QUE" $ \smp -> + getSMPQueueInfo smp rcvPrivateKey rcvId + agentNtfRegisterToken :: AgentClient -> NtfToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519) agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey = withClient c (0, ntfServer, Nothing) "TNEW" $ \ntf -> ntfRegisterToken ntf ntfPrivKey (NewNtfTkn deviceToken ntfPubKey pubDhKey) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 8c23db822..79838792e 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -59,6 +59,7 @@ module Simplex.Messaging.Client connectSMPProxiedRelay, proxySMPMessage, forwardSMPMessage, + getSMPQueueInfo, sendProtocolCommand, -- * Supporting types and client configuration @@ -128,6 +129,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Protocol +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport @@ -920,6 +922,12 @@ forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = pure fwdResponse r -> throwE $ unexpectedResponse r +getSMPQueueInfo :: SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO QueueInfo +getSMPQueueInfo c pKey qId = + sendSMPCommand c (Just pKey) qId QUE >>= \case + INFO info -> pure info + r -> throwE $ unexpectedResponse r + okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 1812dce37..5edea1719 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -156,6 +156,7 @@ module Simplex.Messaging.Protocol sameSrvAddr, sameSrvAddr', noAuthSrv, + toMsgInfo, -- * TCP transport functions TransportBatch (..), @@ -197,8 +198,8 @@ import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) -import Data.Time.Clock.System (SystemTime (..)) +import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Data.Type.Equality import Data.Word (Word16) import qualified Data.X509 as X @@ -210,6 +211,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) @@ -386,6 +388,7 @@ data Command (p :: Party) where ACK :: MsgId -> Command Recipient OFF :: Command Recipient DEL :: Command Recipient + QUE :: Command Recipient -- SMP sender commands -- SEND v1 has to be supported for encoding/decoding -- SEND :: MsgBody -> Command Sender @@ -463,6 +466,7 @@ data BrokerMsg where RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy PRES :: EncResponse -> BrokerMsg -- proxy to client END :: BrokerMsg + INFO :: QueueInfo -> BrokerMsg OK :: BrokerMsg ERR :: ErrorType -> BrokerMsg PONG :: BrokerMsg @@ -505,6 +509,13 @@ data Message 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 @@ -652,6 +663,7 @@ data CommandTag (p :: Party) where ACK_ :: CommandTag Recipient OFF_ :: CommandTag Recipient DEL_ :: CommandTag Recipient + QUE_ :: CommandTag Recipient SEND_ :: CommandTag Sender PING_ :: CommandTag Sender PRXY_ :: CommandTag ProxiedClient @@ -674,6 +686,7 @@ data BrokerMsgTag | RRES_ | PRES_ | END_ + | INFO_ | OK_ | ERR_ | PONG_ @@ -698,6 +711,7 @@ instance PartyI p => Encoding (CommandTag p) where ACK_ -> "ACK" OFF_ -> "OFF" DEL_ -> "DEL" + QUE_ -> "QUE" SEND_ -> "SEND" PING_ -> "PING" PRXY_ -> "PRXY" @@ -717,6 +731,7 @@ instance ProtocolMsgTag CmdTag where "ACK" -> Just $ CT SRecipient ACK_ "OFF" -> Just $ CT SRecipient OFF_ "DEL" -> Just $ CT SRecipient DEL_ + "QUE" -> Just $ CT SRecipient QUE_ "SEND" -> Just $ CT SSender SEND_ "PING" -> Just $ CT SSender PING_ "PRXY" -> Just $ CT SProxiedClient PRXY_ @@ -742,6 +757,7 @@ instance Encoding BrokerMsgTag where RRES_ -> "RRES" PRES_ -> "PRES" END_ -> "END" + INFO_ -> "INFO" OK_ -> "OK" ERR_ -> "ERR" PONG_ -> "PONG" @@ -757,6 +773,7 @@ instance ProtocolMsgTag BrokerMsgTag where "RRES" -> Just RRES_ "PRES" -> Just PRES_ "END" -> Just END_ + "INFO" -> Just INFO_ "OK" -> Just OK_ "ERR" -> Just ERR_ "PONG" -> Just PONG_ @@ -1275,6 +1292,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where ACK msgId -> e (ACK_, ' ', msgId) OFF -> e OFF_ DEL -> e DEL_ + QUE -> e QUE_ SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg) PING -> e PING_ NSUB -> e NSUB_ @@ -1340,6 +1358,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where ACK_ -> ACK <$> _smpP OFF_ -> pure OFF DEL_ -> pure DEL + QUE_ -> pure QUE CT SSender tag -> Cmd SSender <$> case tag of SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) @@ -1368,6 +1387,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) END -> e END_ + INFO info -> e (INFO_, ' ', info) OK -> e OK_ ERR err -> e (ERR_, ' ', err) PONG -> e PONG_ @@ -1388,6 +1408,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) END_ -> pure END + INFO_ -> INFO <$> _smpP OK_ -> pure OK ERR_ -> ERR <$> _smpP PONG_ -> pure PONG diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 0d209229c..d34c0002c 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -59,7 +59,7 @@ import Data.List (intercalate, mapAccumR) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M -import Data.Maybe (catMaybes, fromMaybe, isNothing) +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) @@ -82,6 +82,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.MsgStore import Simplex.Messaging.Server.MsgStore.STM import Simplex.Messaging.Server.QueueStore +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.Server.QueueStore.STM as QS import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog @@ -791,6 +792,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi NDEL -> deleteQueueNotifier_ st OFF -> suspendQueue_ st DEL -> delQueueAndMsgs st + QUE -> withQueue getQueueInfo where createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> M (Transmission BrokerMsg) createQueue st recipientKey dhKey subMode = time "NEW" $ do @@ -1162,6 +1164,26 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Right q -> updateDeletedStats q $> ok Left e -> pure $ err e + getQueueInfo :: QueueRec -> M (Transmission BrokerMsg) + getQueueInfo QueueRec {senderKey, notifier} = do + q@MsgQueue {size} <- getStoreMsgQueue "getQueueInfo" queueId + info <- atomically $ do + qiSub <- TM.lookup queueId subscriptions >>= mapM mkQSub + qiSize <- readTVar size + qiMsg <- toMsgInfo <$$> tryPeekMsg q + pure QueueInfo {qiSnd = isJust senderKey, qiNtf = isJust notifier, qiSub, qiSize, qiMsg} + pure (corrId, queueId, INFO info) + where + mkQSub sub = do + Sub {subThread, delivered} <- readTVar sub + let qSubThread = case subThread of + NoSub -> QNoSub + SubPending -> QSubPending + SubThread _ -> QSubThread + ProhibitSub -> QProhibitSub + qDelivered <- decodeLatin1 . encode <$$> tryReadTMVar delivered + pure QSub {qSubThread, qDelivered} + ok :: Transmission BrokerMsg ok = (corrId, queueId, OK) diff --git a/src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs b/src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs new file mode 100644 index 000000000..b329a54ff --- /dev/null +++ b/src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Messaging.Server.QueueStore.QueueInfo where + +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Text (Text) +import Data.Time.Clock (UTCTime) +import Simplex.Messaging.Encoding +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) +import Simplex.Messaging.Util ((<$?>)) + +data QueueInfo = QueueInfo + { qiSnd :: Bool, + qiNtf :: Bool, + qiSub :: Maybe QSub, + qiSize :: Int, + qiMsg :: Maybe MsgInfo + } + deriving (Eq, Show) + +data QSub = QSub + { qSubThread :: QSubThread, + qDelivered :: Maybe Text + } + deriving (Eq, Show) + +data QSubThread = QNoSub | QSubPending | QSubThread | QProhibitSub + deriving (Eq, Show) + +data MsgInfo = MsgInfo + { msgId :: Text, + msgTs :: UTCTime, + msgType :: MsgType + } + deriving (Eq, Show) + +data MsgType = MTMessage | MTQuota + deriving (Eq, Show) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "Q") ''QSubThread) + +$(JQ.deriveJSON defaultJSON ''QSub) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "MT") ''MsgType) + +$(JQ.deriveJSON defaultJSON ''MsgInfo) + +$(JQ.deriveJSON defaultJSON ''QueueInfo) + +instance Encoding QueueInfo where + smpEncode = LB.toStrict . J.encode + smpP = J.eitherDecodeStrict <$?> A.takeByteString diff --git a/src/Simplex/RemoteControl/Invitation.hs b/src/Simplex/RemoteControl/Invitation.hs index 712c41a9d..d606c4ff0 100644 --- a/src/Simplex/RemoteControl/Invitation.hs +++ b/src/Simplex/RemoteControl/Invitation.hs @@ -123,7 +123,7 @@ instance StrEncoding RCSignedInvitation where idsig <- requiredP sigs "idsig" $ parseAll strP pure RCSignedInvitation {invitation, ssig, idsig} -signInvitation :: C.PrivateKey C.Ed25519 -> C.PrivateKey C.Ed25519 -> RCInvitation -> RCSignedInvitation +signInvitation :: C.PrivateKey 'C.Ed25519 -> C.PrivateKey 'C.Ed25519 -> RCInvitation -> RCSignedInvitation signInvitation sKey idKey invitation = RCSignedInvitation {invitation, ssig, idsig} where uri = strEncode invitation diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 2b21ff3f7..c2badea63 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -57,6 +57,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.Reader import Data.Bifunctor (first) +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (isRight) @@ -66,6 +67,7 @@ import Data.List.NonEmpty (NonEmpty) import qualified Data.Map as M import Data.Maybe (isJust, isNothing) import qualified Data.Set as S +import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.Clock.System (SystemTime (..), getSystemTime) import Data.Type.Equality (testEquality, (:~:) (Refl)) @@ -92,6 +94,7 @@ import Simplex.Messaging.Protocol (BasicAuth, ErrorType (..), MsgBody, ProtocolS import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Server.Expiration +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion, supportedSMPHandshakes) import Simplex.Messaging.Util (diffToMicroseconds) import Simplex.Messaging.Version (VersionRange (..)) @@ -107,8 +110,11 @@ type AEntityTransmission e = (ACorrId, ConnId, ACommand 'Agent e) -- deriving instance Eq (ValidFileDescription p) +shouldRespond :: (HasCallStack, MonadUnliftIO m, Eq a, Show a) => m a -> a -> m () +a `shouldRespond` r = withFrozenCallStack $ withTimeout a (`shouldBe` r) + (##>) :: (HasCallStack, MonadUnliftIO m) => m (AEntityTransmission e) -> AEntityTransmission e -> m () -a ##> t = withTimeout a (`shouldBe` t) +a ##> t = a `shouldRespond` t (=##>) :: (Show a, HasCallStack, MonadUnliftIO m) => m a -> (HasCallStack => a -> Bool) -> m () a =##> p = @@ -228,7 +234,7 @@ mkVersionRange :: Word16 -> Word16 -> VersionRange v mkVersionRange v1 v2 = V.mkVersionRange (Version v1) (Version v2) runRight_ :: (Eq e, Show e, HasCallStack) => ExceptT e IO () -> Expectation -runRight_ action = runExceptT action `shouldReturn` Right () +runRight_ action = withFrozenCallStack $ runExceptT action `shouldReturn` Right () runRight :: (Show e, HasCallStack) => ExceptT e IO a -> IO a runRight action = @@ -444,6 +450,9 @@ functionalAPITests t = do it "should wait for user network" testWaitForUserNetwork it "should not reset online to offline if happens too quickly" testDoNotResetOnlineToOffline it "should resume multiple threads" testResumeMultipleThreads + describe "SMP queue info" $ do + it "server should respond with queue and subscription information" $ + withSmpServer t testServerQueueInfo testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do @@ -2755,6 +2764,91 @@ testResumeMultipleThreads = do where aCfg = agentCfg {userOfflineDelay = 0} +testServerQueueInfo :: IO () +testServerQueueInfo = do + withAgentClients2 $ \alice bob -> runRight_ $ do + (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + liftIO $ threadDelay 200000 + checkEmptyQ alice bobId False + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe + ("", _, CONF confId _ "bob's connInfo") <- get alice + liftIO $ threadDelay 200000 + checkEmptyQ alice bobId False + allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, CON) + get bob ##> ("", aliceId, INFO "alice's connInfo") + get bob ##> ("", aliceId, CON) + liftIO $ threadDelay 200000 + checkEmptyQ alice bobId True + checkEmptyQ bob aliceId True + let msgId = 4 + (msgId', PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello" + liftIO $ msgId' `shouldBe` msgId + get alice ##> ("", bobId, SENT msgId) + liftIO $ threadDelay 200000 + Just srvMsgId <- checkMsgQ bob aliceId 1 + get bob =##> \case + ("", c, MSG MsgMeta {integrity = MsgOk, broker = (smId, _), recipient = (mId, _), pqEncryption = PQEncOn} _ "hello") -> + c == aliceId && decodeLatin1 (B64.encode smId) == srvMsgId && mId == msgId + _ -> False + ackMessage bob aliceId msgId Nothing + liftIO $ threadDelay 200000 + checkEmptyQ bob aliceId True + (msgId1, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 1" + get alice ##> ("", bobId, SENT msgId1) + Just _ <- checkMsgQ bob aliceId 1 + (msgId2, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 2" + get alice ##> ("", bobId, SENT msgId2) + (msgId3, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 3" + get alice ##> ("", bobId, SENT msgId3) + (msgId4, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 4" + get alice ##> ("", bobId, SENT msgId4) + Just _ <- checkMsgQ bob aliceId 4 + (msgId5, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello: quota exceeded" + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 5 + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 1") -> c == aliceId && mId == msgId1; _ -> False + ackMessage bob aliceId msgId1 Nothing + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 4 + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 2") -> c == aliceId && mId == msgId2; _ -> False + ackMessage bob aliceId msgId2 Nothing + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 3") -> c == aliceId && mId == msgId3; _ -> False + ackMessage bob aliceId msgId3 Nothing + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 2 + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 4") -> c == aliceId && mId == msgId4; _ -> False + ackMessage bob aliceId msgId4 Nothing + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 1 -- the one that did not fit now accepted + get alice ##> ("", bobId, QCONT) + get alice ##> ("", bobId, SENT msgId5) + liftIO $ threadDelay 200000 + Just _srvMsgId <- checkQ bob aliceId True (Just QNoSub) 1 (Just MTMessage) + get bob =##> \case ("", c, Msg' mId PQEncOn "hello: quota exceeded") -> c == aliceId && mId == msgId5 + 1; _ -> False + ackMessage bob aliceId (msgId5 + 1) Nothing + liftIO $ threadDelay 200000 + checkEmptyQ bob aliceId True + pure () + where + checkEmptyQ c cId qiSnd' = do + r <- checkQ c cId qiSnd' (Just QSubThread) 0 Nothing + liftIO $ r `shouldBe` Nothing + checkMsgQ c cId qiSize' = do + r <- checkQ c cId True (Just QNoSub) qiSize' (Just MTMessage) + liftIO $ isJust r `shouldBe` True + pure r + checkQ c cId qiSnd' qiSubThread_ qiSize' msgType_ = do + QueueInfo {qiSnd, qiNtf, qiSub, qiSize, qiMsg} <- getConnectionQueueInfo c cId + liftIO $ do + qiSnd `shouldBe` qiSnd' + qiNtf `shouldBe` False + qSubThread <$> qiSub `shouldBe` qiSubThread_ + qiSize `shouldBe` qiSize' + msgId_ <- forM qiMsg $ \MsgInfo {msgId, msgType} -> msgId <$ (Just msgType `shouldBe` msgType_) + qDelivered <$> qiSub `shouldBe` Just msgId_ + pure msgId_ + noNetworkDelay :: AgentClient -> IO () noNetworkDelay a = do d <- waitNetwork a diff --git a/tests/CLITests.hs b/tests/CLITests.hs index 2193de5e9..1310665ee 100644 --- a/tests/CLITests.hs +++ b/tests/CLITests.hs @@ -77,13 +77,13 @@ smpServerTest storeLog basicAuth = do let certPath = cfgPath "server.crt" oldCrt@X.Certificate {} <- XF.readSignedObject certPath >>= \case - [cert] -> pure . X.signedObject $ X.getSigned cert + [cert'] -> pure . X.signedObject $ X.getSigned cert' _ -> error "bad crt format" r' <- lines <$> capture_ (withArgs ["cert"] $ (100000 `timeout` smpServerCLI cfgPath logPath) `catchAll_` pure (Just ())) r' `shouldContain` ["Generated new server credentials"] newCrt <- XF.readSignedObject certPath >>= \case - [cert] -> pure . X.signedObject $ X.getSigned cert + [cert'] -> pure . X.signedObject $ X.getSigned cert' _ -> error "bad crt format after cert" X.certSignatureAlg oldCrt `shouldBe` X.certSignatureAlg newCrt X.certSubjectDN oldCrt `shouldBe` X.certSubjectDN newCrt From 6e4067dc0cfb726a86b83e2641531a2ef8da71eb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 09:16:00 +0100 Subject: [PATCH 080/125] add string encodings for SMPProxyMode and SMPProxyFallback --- src/Simplex/Messaging/Client.hs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 79838792e..56ebf7a3f 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -291,6 +291,32 @@ data SMPProxyFallback | SPFProhibit -- prohibit direct connection to destination relay. deriving (Eq, Show) +instance StrEncoding SMPProxyMode where + strEncode = \case + SPMAlways -> "always" + SPMUnknown -> "unknown" + SPMUnprotected -> "unprotected" + SPMNever -> "never" + strP = + A.takeTill (== ' ') >>= \case + "always" -> pure SPMAlways + "unknown" -> pure SPMUnknown + "unprotected" -> pure SPMUnprotected + "never" -> pure SPMNever + _ -> fail "Invalid SMP proxy mode" + +instance StrEncoding SMPProxyFallback where + strEncode = \case + SPFAllow -> "yes" + SPFAllowProtected -> "protected" + SPFProhibit -> "no" + strP = + A.takeTill (== ' ') >>= \case + "yes" -> pure SPFAllow + "protected" -> pure SPFAllowProtected + "no" -> pure SPFProhibit + _ -> fail "Invalid SMP proxy fallback mode" + defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig From d12ea9205537a570f369fc493ce3e7891511f623 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 09:47:47 +0100 Subject: [PATCH 081/125] agent: report correct errors from xftp handshake so they are treated as temporary (#1184) * agent: report correct errors from xftp handshake so they are treated as temporary * disable slow servers test * remove comments * all tests * remove duplicate functions --- package.yaml | 3 +++ src/Simplex/FileTransfer/Client.hs | 27 +++++++++++++++---------- src/Simplex/FileTransfer/Server.hs | 22 ++++++++++++++++++++ src/Simplex/FileTransfer/Server/Env.hs | 3 ++- src/Simplex/FileTransfer/Server/Main.hs | 3 ++- src/Simplex/Messaging/Agent.hs | 7 ------- tests/XFTPAgent.hs | 8 ++++++-- tests/XFTPClient.hs | 3 ++- 8 files changed, 53 insertions(+), 23 deletions(-) diff --git a/package.yaml b/package.yaml index 6bdb03e4d..7b9486ff4 100644 --- a/package.yaml +++ b/package.yaml @@ -87,6 +87,9 @@ flags: manual: True default: True +# cpp-options: +# - -Dslow_servers + when: - condition: flag(swift) cpp-options: diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index ff7742e67..2454a1d57 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -52,7 +52,7 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (ALPN, THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client @@ -98,6 +98,12 @@ defaultXFTPClientConfig = clientALPN = Just supportedXFTPhandshakes } +http2XFTPClientError :: HTTP2ClientError -> XFTPClientError +http2XFTPClientError = \case + HCResponseTimeout -> PCEResponseTimeout + HCNetworkError -> PCENetworkError + HCIOError e -> PCEIOError e + getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do let username = proxyUsername transportSession @@ -116,8 +122,7 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN thParams@THandleParams {thVersion} <- case sessionALPN of Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 - Nothing -> pure thParams0 - _ -> throwError $ PCETransportError TEVersion + _ -> pure thParams0 logDebug $ "Client negotiated protocol: " <> tshow thVersion let c = XFTPClient {http2Client, thParams, transportSession, config} atomically $ writeTVar clientVar $ Just c @@ -135,15 +140,15 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session getServerHandshake = do let helloReq = H.requestNoBody "POST" "/" [] HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- - liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing - liftHS . smpDecode =<< liftHS (C.unPad shsBody) + liftError' http2XFTPClientError $ sendRequest c helloReq Nothing + liftTransportErr (TEHandshake PARSE) . smpDecode =<< liftTransportErr TEBadBlock (C.unPad shsBody) processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do - unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION + unless (sessionId == serverSessId) $ throwError $ PCETransportError TEBadSession case xftpVersionRange `compatibleVRange` serverVRange of Nothing -> throwError $ PCETransportError TEVersion Just (Compatible vr) -> - fmap (vr,) . liftHS $ do + fmap (vr,) . liftTransportErr (TEHandshake BAD_AUTH) $ do let (X.CertificateChain cert, exact) = serverAuth case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () @@ -152,11 +157,11 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session C.x509ToPublic (pubKey, []) >>= C.pubKey sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO () sendClientHandshake chs = do - chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize + chs' <- liftTransportErr TELargeMsg $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' - HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c chsReq Nothing - unless (B.null bodyHead) $ throwError $ PCEResponseError HANDSHAKE - liftHS = liftEitherWith (const $ PCEResponseError HANDSHAKE) + HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' http2XFTPClientError $ sendRequest c chsReq Nothing + unless (B.null bodyHead) $ throwError $ PCETransportError TEBadBlock + liftTransportErr e = liftEitherWith (const $ PCETransportError e) closeXFTPClient :: XFTPClient -> IO () closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index ea18b4fdc..7eb65ced2 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} @@ -67,6 +68,9 @@ import Simplex.Messaging.Version import System.Exit (exitFailure) import System.FilePath (()) import System.IO (hPrint, hPutStrLn, universalNewlineMode) +#ifdef slow_servers +import System.Random (getStdRandom, randomR) +#endif import UnliftIO import UnliftIO.Concurrent (threadDelay) import UnliftIO.Directory (doesFileExist, removeFile, renameFile) @@ -138,6 +142,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k) let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey} shs <- encodeXftp hs +#ifdef slow_servers + lift randomDelay +#endif liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing processClientHandshake pk = do @@ -151,6 +158,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} thParams = thParams0 {thAuth = Just auth, thVersion = v, thServerVRange = vr} atomically $ TM.insert sessionId (HandshakeAccepted thParams) sessions +#ifdef slow_servers + lift randomDelay +#endif liftIO . sendResponse $ H.responseNoBody N.ok200 [] pure Nothing Nothing -> throwError HANDSHAKE @@ -315,6 +325,9 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea where sendXFTPResponse (corrId, fId, resp) serverFile_ = do let t_ = xftpEncodeTransmission thParams (corrId, fId, resp) +#ifdef slow_servers + randomDelay +#endif liftIO $ sendResponse $ H.responseStreaming N.ok200 [] $ streamBody t_ where streamBody t_ send done = do @@ -329,6 +342,15 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea withFile filePath ReadMode $ \h -> sendEncFile h send sbState fileSize done +#ifdef slow_servers +randomDelay :: M () +randomDelay = do + d <- asks $ responseDelay . config + when (d > 0) $ do + pc <- getStdRandom (randomR (-200, 200)) + threadDelay $ (d * (1000 + pc)) `div` 1000 +#endif + data VerificationResult = VRVerified XFTPRequest | VRFailed verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult diff --git a/src/Simplex/FileTransfer/Server/Env.hs b/src/Simplex/FileTransfer/Server/Env.hs index 58c1393f3..f8a6bc996 100644 --- a/src/Simplex/FileTransfer/Server/Env.hs +++ b/src/Simplex/FileTransfer/Server/Env.hs @@ -69,7 +69,8 @@ data XFTPServerConfig = XFTPServerConfig logStatsStartTime :: Int64, serverStatsLogFile :: FilePath, serverStatsBackupFile :: Maybe FilePath, - transportConfig :: TransportServerConfig + transportConfig :: TransportServerConfig, + responseDelay :: Int } defaultInactiveClientExpiration :: ExpirationConfig diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index ee9a52d34..d31295cd2 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -185,7 +185,8 @@ xftpServerCLI cfgPath logPath = do defaultTransportServerConfig { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, alpn = Just supportedXFTPhandshakes - } + }, + responseDelay = 0 } data CliCommand diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 073660ba0..8e9020b7d 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -83,8 +83,6 @@ module Simplex.Messaging.Agent testProtocolServer, setNtfServers, setNetworkConfig, - getNetworkConfig, - getNetworkConfig', setUserNetworkInfo, reconnectAllServers, registerNtfToken, @@ -434,11 +432,6 @@ setNetworkConfig c@AgentClient {useNetworkConfig} cfg' = do else True <$ (writeTVar useNetworkConfig $! (slowNetworkConfig cfg', cfg')) when changed $ reconnectAllServers c --- returns fast network config -getNetworkConfig :: AgentClient -> IO NetworkConfig -getNetworkConfig = getNetworkConfig' -{-# INLINE getNetworkConfig #-} - setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO () setUserNetworkInfo c@AgentClient {userNetworkInfo, userNetworkUpdated} ni = withAgentEnv' c $ do ts' <- liftIO getCurrentTime diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 4685d4815..e4cf3d704 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -46,7 +46,11 @@ import XFTPClient xftpAgentTests :: Spec xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do - it "should send and receive file" testXFTPAgentSendReceive + it "should send and receive file" $ withXFTPServer testXFTPAgentSendReceive + -- uncomment CPP option slow_servers and run hpack to run this test + xit "should send and receive file with slow server responses" $ + withXFTPServerCfg testXFTPServerConfig {responseDelay = 500000} $ + \_ -> testXFTPAgentSendReceive it "should send and receive with encrypted local files" testXFTPAgentSendReceiveEncrypted it "should send and receive large file with a redirect" testXFTPAgentSendReceiveRedirect it "should send and receive small file without a redirect" testXFTPAgentSendReceiveNoRedirect @@ -100,7 +104,7 @@ checkProgress (prev, expected) (progress, total) loop | otherwise = pure () testXFTPAgentSendReceive :: HasCallStack => IO () -testXFTPAgentSendReceive = withXFTPServer $ do +testXFTPAgentSendReceive = do filePath <- createRandomFile -- send file, delete snd file internally (rfd1, rfd2) <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do diff --git a/tests/XFTPClient.hs b/tests/XFTPClient.hs index 0152625a8..208b54dc5 100644 --- a/tests/XFTPClient.hs +++ b/tests/XFTPClient.hs @@ -124,7 +124,8 @@ testXFTPServerConfig_ alpn = logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, - transportConfig = defaultTransportServerConfig {alpn} + transportConfig = defaultTransportServerConfig {alpn}, + responseDelay = 0 } testXFTPClientConfig :: XFTPClientConfig From 3a3a84c58c53313906339973adb3caa3fb602b10 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 12:20:29 +0100 Subject: [PATCH 082/125] server: log proxy connection errors --- src/Simplex/Messaging/Server.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index d34c0002c..88e3bf4a3 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -714,6 +714,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi let own = isOwnServer a srv inc own pRequests inc own $ if temporaryClientError e then pErrorsConnect else pErrorsOther + logError $ "Error connecting: " <> decodeLatin1 (strEncode $ host srv) <> " " <> tshow e pure . ERR $ smpProxyError e where proxyResp smp = From 8ed54b33e0f3396721352cfa6546edcc2aedb824 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 09:47:47 +0100 Subject: [PATCH 083/125] agent: report correct errors from xftp handshake so they are treated as temporary (#1184) * agent: report correct errors from xftp handshake so they are treated as temporary * disable slow servers test * remove comments * all tests * remove duplicate functions --- package.yaml | 3 +++ src/Simplex/FileTransfer/Client.hs | 29 +++++++++++++++---------- src/Simplex/FileTransfer/Server.hs | 22 +++++++++++++++++++ src/Simplex/FileTransfer/Server/Env.hs | 3 ++- src/Simplex/FileTransfer/Server/Main.hs | 3 ++- tests/XFTPAgent.hs | 8 +++++-- tests/XFTPClient.hs | 3 ++- 7 files changed, 54 insertions(+), 17 deletions(-) diff --git a/package.yaml b/package.yaml index 49dc27c0c..0eaa36d79 100644 --- a/package.yaml +++ b/package.yaml @@ -85,6 +85,9 @@ flags: manual: True default: True +# cpp-options: +# - -Dslow_servers + when: - condition: flag(swift) cpp-options: diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index e407279ce..49b278e14 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -50,7 +50,7 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client @@ -96,6 +96,12 @@ defaultXFTPClientConfig = clientALPN = Just supportedXFTPhandshakes } +http2XFTPClientError :: HTTP2ClientError -> XFTPClientError +http2XFTPClientError = \case + HCResponseTimeout -> PCEResponseTimeout + HCNetworkError -> PCENetworkError + HCIOError e -> PCEIOError e + getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = clientALPN} @@ -112,8 +118,7 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN thParams@THandleParams {thVersion} <- case sessionALPN of Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 - Nothing -> pure thParams0 - _ -> throwError $ PCETransportError (TEHandshake VERSION) + _ -> pure thParams0 logDebug $ "Client negotiated protocol: " <> tshow thVersion let c = XFTPClient {http2Client, thParams, transportSession, config} atomically $ writeTVar clientVar $ Just c @@ -130,15 +135,15 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session getServerHandshake = do let helloReq = H.requestNoBody "POST" "/" [] HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- - liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing - liftHS . smpDecode =<< liftHS (C.unPad shsBody) + liftError' http2XFTPClientError $ sendRequest c helloReq Nothing + liftTransportErr (TEHandshake PARSE) . smpDecode =<< liftTransportErr TEBadBlock (C.unPad shsBody) processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do - unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION + unless (sessionId == serverSessId) $ throwError $ PCETransportError TEBadSession case xftpVersionRange `compatibleVersion` serverVRange of - Nothing -> throwError $ PCEResponseError HANDSHAKE + Nothing -> throwError $ PCETransportError (TEHandshake VERSION) Just (Compatible v) -> - fmap (v,) . liftHS $ do + fmap (v,) . liftTransportErr (TEHandshake BAD_AUTH) $ do let (X.CertificateChain cert, exact) = serverAuth case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () @@ -147,11 +152,11 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session C.x509ToPublic (pubKey, []) >>= C.pubKey sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO () sendClientHandshake chs = do - chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize + chs' <- liftTransportErr TELargeMsg $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' - HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c chsReq Nothing - unless (B.null bodyHead) $ throwError $ PCEResponseError HANDSHAKE - liftHS = liftEitherWith (const $ PCEResponseError HANDSHAKE) + HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' http2XFTPClientError $ sendRequest c chsReq Nothing + unless (B.null bodyHead) $ throwError $ PCETransportError TEBadBlock + liftTransportErr e = liftEitherWith (const $ PCETransportError e) closeXFTPClient :: XFTPClient -> IO () closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 7b6787a43..6b9c81a26 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} @@ -67,6 +68,9 @@ import Simplex.Messaging.Version (isCompatible) import System.Exit (exitFailure) import System.FilePath (()) import System.IO (hPrint, hPutStrLn, universalNewlineMode) +#ifdef slow_servers +import System.Random (getStdRandom, randomR) +#endif import UnliftIO import UnliftIO.Concurrent (threadDelay) import UnliftIO.Directory (doesFileExist, removeFile, renameFile) @@ -136,6 +140,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k) let hs = XFTPServerHandshake {xftpVersionRange = supportedFileServerVRange, sessionId, authPubKey} shs <- encodeXftp hs +#ifdef slow_servers + lift randomDelay +#endif liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing processClientHandshake pk = do @@ -147,6 +154,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions +#ifdef slow_servers + lift randomDelay +#endif liftIO . sendResponse $ H.responseNoBody N.ok200 [] pure Nothing sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) @@ -310,6 +320,9 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea where sendXFTPResponse (corrId, fId, resp) serverFile_ = do let t_ = xftpEncodeTransmission thParams (corrId, fId, resp) +#ifdef slow_servers + randomDelay +#endif liftIO $ sendResponse $ H.responseStreaming N.ok200 [] $ streamBody t_ where streamBody t_ send done = do @@ -324,6 +337,15 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea withFile filePath ReadMode $ \h -> sendEncFile h send sbState (fromIntegral fileSize) done +#ifdef slow_servers +randomDelay :: M () +randomDelay = do + d <- asks $ responseDelay . config + when (d > 0) $ do + pc <- getStdRandom (randomR (-200, 200)) + threadDelay $ (d * (1000 + pc)) `div` 1000 +#endif + data VerificationResult = VRVerified XFTPRequest | VRFailed verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult diff --git a/src/Simplex/FileTransfer/Server/Env.hs b/src/Simplex/FileTransfer/Server/Env.hs index 414bfb4c4..76cafcad4 100644 --- a/src/Simplex/FileTransfer/Server/Env.hs +++ b/src/Simplex/FileTransfer/Server/Env.hs @@ -66,7 +66,8 @@ data XFTPServerConfig = XFTPServerConfig logStatsStartTime :: Int64, serverStatsLogFile :: FilePath, serverStatsBackupFile :: Maybe FilePath, - transportConfig :: TransportServerConfig + transportConfig :: TransportServerConfig, + responseDelay :: Int } defaultInactiveClientExpiration :: ExpirationConfig diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index d53b3f4fa..271259451 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -182,7 +182,8 @@ xftpServerCLI cfgPath logPath = do defaultTransportServerConfig { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, alpn = Just supportedXFTPhandshakes - } + }, + responseDelay = 0 } data CliCommand diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 88786bb40..2deb1c655 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -46,7 +46,11 @@ import XFTPClient xftpAgentTests :: Spec xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do - it "should send and receive file" testXFTPAgentSendReceive + it "should send and receive file" $ withXFTPServer testXFTPAgentSendReceive + -- uncomment CPP option slow_servers and run hpack to run this test + xit "should send and receive file with slow server responses" $ + withXFTPServerCfg testXFTPServerConfig {responseDelay = 500000} $ + \_ -> testXFTPAgentSendReceive it "should send and receive with encrypted local files" testXFTPAgentSendReceiveEncrypted it "should send and receive large file with a redirect" testXFTPAgentSendReceiveRedirect it "should send and receive small file without a redirect" testXFTPAgentSendReceiveNoRedirect @@ -100,7 +104,7 @@ checkProgress (prev, expected) (progress, total) loop | otherwise = pure () testXFTPAgentSendReceive :: HasCallStack => IO () -testXFTPAgentSendReceive = withXFTPServer $ do +testXFTPAgentSendReceive = do filePath <- createRandomFile -- send file, delete snd file internally (rfd1, rfd2) <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do diff --git a/tests/XFTPClient.hs b/tests/XFTPClient.hs index 5f38cc639..a3ee9dd1f 100644 --- a/tests/XFTPClient.hs +++ b/tests/XFTPClient.hs @@ -122,7 +122,8 @@ testXFTPServerConfig_ alpn = logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, - transportConfig = defaultTransportServerConfig {alpn} + transportConfig = defaultTransportServerConfig {alpn}, + responseDelay = 0 } testXFTPClientConfig :: XFTPClientConfig From 0b5ab3a374ff0ed9719a77fdda03a296e49eb78b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 14:23:21 +0100 Subject: [PATCH 084/125] 5.7.6.0 --- CHANGELOG.md | 5 +++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b521e0e9..6d6f90891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 5.7.6 + +XFTP agent: +- treat XFTP handshake timeouts and network errors as temporary, to retry file operations. + # 5.7.5 SMP agent: diff --git a/package.yaml b/package.yaml index 0eaa36d79..8277c7ab1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.7.5.0 +version: 5.7.6.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 2e7d7bb53..d7ccc0d1d 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.7.5.0 +version: 5.7.6.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From e1017e2a7ffff80a779f66fbb99189849b68b547 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 14:25:57 +0100 Subject: [PATCH 085/125] 5.8.0.9 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 7b9486ff4..de34cb103 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.8 +version: 5.8.0.9 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 31dfff436..ce62b8446 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.8 +version: 5.8.0.9 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From d28b17e787e2729768d924f5231583fb7d112d93 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 22:18:28 +0100 Subject: [PATCH 086/125] xftp server: send HTTP2 error as timeout error to the client so it is treated as temporary (#1186) * xftp server: log file reception error * report HTTP2 error as timeout error * reduce timeout to 5 min * process timeout error in protocol response * log warning on timeout/HTTP2 error --- src/Simplex/FileTransfer/Client.hs | 10 ++-------- src/Simplex/FileTransfer/Server.hs | 2 +- src/Simplex/FileTransfer/Server/Main.hs | 2 +- src/Simplex/FileTransfer/Transport.hs | 14 +++++++++++--- src/Simplex/Messaging/Agent/Client.hs | 15 +++++++++------ 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 2454a1d57..44d1b596b 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -98,12 +98,6 @@ defaultXFTPClientConfig = clientALPN = Just supportedXFTPhandshakes } -http2XFTPClientError :: HTTP2ClientError -> XFTPClientError -http2XFTPClientError = \case - HCResponseTimeout -> PCEResponseTimeout - HCNetworkError -> PCENetworkError - HCIOError e -> PCEIOError e - getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do let username = proxyUsername transportSession @@ -140,7 +134,7 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session getServerHandshake = do let helloReq = H.requestNoBody "POST" "/" [] HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- - liftError' http2XFTPClientError $ sendRequest c helloReq Nothing + liftError' xftpClientError $ sendRequest c helloReq Nothing liftTransportErr (TEHandshake PARSE) . smpDecode =<< liftTransportErr TEBadBlock (C.unPad shsBody) processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do @@ -159,7 +153,7 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session sendClientHandshake chs = do chs' <- liftTransportErr TELargeMsg $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' - HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' http2XFTPClientError $ sendRequest c chsReq Nothing + HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' xftpClientError $ sendRequest c chsReq Nothing unless (B.null bodyHead) $ throwError $ PCETransportError TEBadBlock liftTransportErr e = liftEitherWith (const $ PCETransportError e) diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 7eb65ced2..1ed4894ec 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -479,7 +479,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case pure $ FRErr e receiveChunk spec = do t <- asks $ fileTimeout . config - liftIO $ fromMaybe (Left TIMEOUT) <$> timeout t (runExceptT (receiveFile getBody spec) `catchAll_` pure (Left FILE_IO)) + liftIO $ fromMaybe (Left TIMEOUT) <$> timeout t (runExceptT $ receiveFile getBody spec) sendServerFile :: FileRec -> RcvPublicDhKey -> M (FileResponse, Maybe ServerFile) sendServerFile FileRec {senderId, filePath, fileInfo = FileInfo {size}} rDhKey = do readTVarIO filePath >>= \case diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index d31295cd2..76b1f157a 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -166,7 +166,7 @@ xftpServerCLI cfgPath logPath = do defaultFileExpiration { ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini }, - fileTimeout = 10 * 60 * 1000000, -- 10 mins to send 4mb chunk + fileTimeout = 5 * 60 * 1000000, -- 5 mins to send 4mb chunk inactiveClientExpiration = settingIsOn "INACTIVE_CLIENTS" "disconnect" ini $> ExpirationConfig diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 2f0a5de4f..935fa1c42 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -34,6 +34,7 @@ where import Control.Applicative ((<|>)) import qualified Control.Exception as E +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class @@ -46,8 +47,10 @@ import Data.ByteString.Builder (Builder, byteString) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Functor (($>)) import Data.Word (Word16, Word32) import qualified Data.X509 as X +import Network.HTTP2.Client (HTTP2Error) import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding @@ -56,7 +59,7 @@ import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol (CommandError) import Simplex.Messaging.Transport (SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.File -import Simplex.Messaging.Util (bshow) +import Simplex.Messaging.Util (bshow, tshow) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.IO (Handle, IOMode (..), withFile) @@ -145,9 +148,14 @@ sendEncFile h send = go go sbState' $ sz - fromIntegral (B.length ch) receiveFile :: (Int -> IO ByteString) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO () -receiveFile getBody = receiveFile_ receive +receiveFile getBody chunk = ExceptT $ runExceptT (receiveFile_ receive chunk) `E.catches` handlers where receive h sz = hReceiveFile getBody h sz >>= \sz' -> pure $ if sz' == 0 then Right () else Left SIZE + handlers = + [ E.Handler $ \(e :: HTTP2Error) -> logWarn (err e) $> Left TIMEOUT, + E.Handler $ \(e :: E.SomeException) -> logError (err e) $> Left FILE_IO + ] + err e = "receiveFile error: " <> tshow e receiveEncFile :: (Int -> IO ByteString) -> LC.SbState -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO () receiveEncFile getBody = receiveFile_ . receive @@ -213,7 +221,7 @@ data XFTPErrorType HAS_FILE | -- | file IO error FILE_IO - | -- | file sending timeout + | -- | file sending or receiving timeout TIMEOUT | -- | bad redirect data REDIRECT {redirectError :: String} diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index e63ab5d1d..e18888244 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -188,6 +188,7 @@ import qualified Simplex.FileTransfer.Client as X import Simplex.FileTransfer.Description (ChunkReplicaId (..), FileDigest (..), kb) import Simplex.FileTransfer.Protocol (FileInfo (..), FileResponse) import Simplex.FileTransfer.Transport (XFTPErrorType (DIGEST), XFTPRcvChunkSpec (..), XFTPVersion) +import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (DeletedSndChunkReplica (..), NewSndChunkReplica (..), RcvFileChunkReplica (..), SndFileChunk (..), SndFileChunkReplica (..)) import Simplex.FileTransfer.Util (uniqueCombine) import Simplex.Messaging.Agent.Env.SQLite @@ -1315,6 +1316,7 @@ temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case BROKER _ e -> tempBrokerError e SMP _ (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e + XFTP _ XFTP.TIMEOUT -> True PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e PROXY _ _ (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> True INACTIVE -> True @@ -2081,12 +2083,13 @@ getAgentQueuesInfo AgentClient {msgQ, subQ, smpClients} = do where getClientQueuesInfo :: SMPClientVar -> IO (Int, UTCTime, ClientInfo) getClientQueuesInfo SessionVar {sessionVar, sessionVarId, sessionVarTs} = do - clientInfo <- atomically (tryReadTMVar sessionVar) >>= \case - Just (Right c) -> do - (sndQInfo, rcvQInfo) <- getProtocolClientQueuesInfo $ protocolClient c - pure ClientInfoQueues {sndQInfo, rcvQInfo} - Just (Left e) -> pure $ ClientInfoError e - Nothing -> pure ClientInfoConnecting + clientInfo <- + atomically (tryReadTMVar sessionVar) >>= \case + Just (Right c) -> do + (sndQInfo, rcvQInfo) <- getProtocolClientQueuesInfo $ protocolClient c + pure ClientInfoQueues {sndQInfo, rcvQInfo} + Just (Left e) -> pure $ ClientInfoError e + Nothing -> pure ClientInfoConnecting pure (sessionVarId, sessionVarTs, clientInfo) $(J.deriveJSON defaultJSON ''AgentLocks) From 2e4f507919dd3a08e5c4106fde193fee4414de3c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 31 May 2024 22:20:30 +0100 Subject: [PATCH 087/125] 5.8.0.10 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index de34cb103..662cf8a0a 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.0.9 +version: 5.8.0.10 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index ce62b8446..4974e9f75 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.0.9 +version: 5.8.0.10 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From 44b8f265ae1d9a8dffe4b74605b5c68d05afcfe6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 3 Jun 2024 10:01:59 +0100 Subject: [PATCH 088/125] 5.8.0: changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6f90891..e161a26df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# 5.8.0 + +Version 5.8.0.10 + +SMP server and client: +- protocol extension to forward messages to the destination servers, to protect sending client IP address and transport session. + +Agent: +- process timed out subscription responses to reduce the number of resubscriptions. +- avoid sending messages and commands when waiting for response timed out (except batched SUB and DEL commands). +- fix issue with stuck message reception on slow connection (when response to ACK timed out, and the new message was not processed until resubscribed). +- fix issue when temporary file sending or receiving error was treated as permanent. + +SMP server: +- include OK responses to all batched SUB requests to reduce subscription timeouts. + +XFTP server: +- report file upload timeout as TIMEOUT, to avoid delivery failure. + # 5.7.6 XFTP agent: From 3dab33048012be3e78005482b661be29211875a0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 5 Jun 2024 11:20:50 +0100 Subject: [PATCH 089/125] use throwE instead of throwError (#1187) * use throwE instead of throwError * test delay --- src/Simplex/FileTransfer/Agent.hs | 41 +++++----- src/Simplex/FileTransfer/Client.hs | 20 ++--- src/Simplex/FileTransfer/Client/Agent.hs | 3 +- src/Simplex/FileTransfer/Client/Main.hs | 25 +++--- src/Simplex/FileTransfer/Crypto.hs | 13 +-- src/Simplex/FileTransfer/Server.hs | 11 +-- src/Simplex/FileTransfer/Transport.hs | 2 +- src/Simplex/Messaging/Agent.hs | 82 +++++++++---------- src/Simplex/Messaging/Agent/Client.hs | 22 ++--- src/Simplex/Messaging/Agent/Store/SQLite.hs | 3 +- src/Simplex/Messaging/Client.hs | 4 +- src/Simplex/Messaging/Crypto/File.hs | 7 +- src/Simplex/Messaging/Crypto/Ratchet.hs | 2 +- .../Notifications/Server/Push/APNS.hs | 17 ++-- .../Messaging/Notifications/Transport.hs | 4 +- src/Simplex/Messaging/Util.hs | 7 +- src/Simplex/RemoteControl/Client.hs | 35 ++++---- tests/AgentTests/FunctionalAPITests.hs | 2 +- 18 files changed, 155 insertions(+), 145 deletions(-) diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index c8030b206..654a83207 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -32,6 +32,7 @@ import Control.Logger.Simple (logError) import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Control.Monad.Trans.Except import Data.Bifunctor (first) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB @@ -141,7 +142,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi downloadChunk :: AgentClient -> FileChunk -> AM () downloadChunk c FileChunk {replicas = (FileChunkReplica {server} : _)} = do lift . void $ getXFTPRcvWorker True c (Just server) -downloadChunk _ _ = throwError $ INTERNAL "no replicas" +downloadChunk _ _ = throwE $ INTERNAL "no replicas" getPrefixPath :: String -> AM' FilePath getPrefixPath suffix = do @@ -194,7 +195,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do retryDone = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM () downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do - unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwError $ XFTP "" XFTP.NOT_APPROVED + unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ XFTP "" XFTP.NOT_APPROVED fsFileTmpPath <- lift $ toFSFilePath fileTmpPath chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest) @@ -267,9 +268,9 @@ runXFTPRcvLocalWorker c Worker {doWork} = do withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting chunkPaths <- getChunkPaths chunks encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths - when (FileSize encSize /= size) $ throwError $ XFTP "" XFTP.SIZE + when (FileSize encSize /= size) $ throwE $ XFTP "" XFTP.SIZE encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths - when (FileDigest encDigest /= digest) $ throwError $ XFTP "" XFTP.DIGEST + when (FileDigest encDigest /= digest) $ throwE $ XFTP "" XFTP.DIGEST let destFile = CryptoFile fsSavePath cfArgs void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile case redirect of @@ -287,10 +288,10 @@ runXFTPRcvLocalWorker c Worker {doWork} = do yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath) next@FileDescription {chunks = nextChunks} <- case strDecode (LB.toStrict yaml) of -- TODO switch to another error constructor - Left _ -> throwError . XFTP "" $ XFTP.REDIRECT "decode error" + Left _ -> throwE . XFTP "" $ XFTP.REDIRECT "decode error" Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest}) - | dstSize /= redirectSize -> throwError . XFTP "" $ XFTP.REDIRECT "size mismatch" - | dstDigest /= redirectDigest -> throwError . XFTP "" $ XFTP.REDIRECT "digest mismatch" + | dstSize /= redirectSize -> throwE . XFTP "" $ XFTP.REDIRECT "size mismatch" + | dstDigest /= redirectDigest -> throwE . XFTP "" $ XFTP.REDIRECT "digest mismatch" | otherwise -> pure fd -- register and download chunks from the actual file withStore c $ \db -> updateRcvFileRedirect db redirectDbId next @@ -303,7 +304,7 @@ runXFTPRcvLocalWorker c Worker {doWork} = do fsPath <- lift $ toFSFilePath path pure $ fsPath : ps getChunkPaths (RcvFileChunk {chunkTmpPath = Nothing} : _cs) = - throwError $ INTERNAL "no chunk path" + throwE $ INTERNAL "no chunk path" xftpDeleteRcvFile' :: AgentClient -> RcvFileId -> AM' () xftpDeleteRcvFile' c rcvFileEntityId = xftpDeleteRcvFiles' c [rcvFileEntityId] @@ -379,7 +380,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do prepareFile cfg f `catchAgentError` (sndWorkerInternalError c sndFileId sndFileEntityId prefixPath . show) prepareFile :: AgentConfig -> SndFile -> AM () prepareFile _ SndFile {prefixPath = Nothing} = - throwError $ INTERNAL "no prefix path" + throwE $ INTERNAL "no prefix path" prepareFile cfg sndFile@SndFile {sndFileId, userId, prefixPath = Just ppath, status} = do SndFile {numRecipients, chunks} <- if status /= SFSEncrypted -- status is SFSNew or SFSEncrypting @@ -405,14 +406,14 @@ runXFTPSndPrepareWorker c Worker {doWork} = do let CryptoFile {filePath} = srcFile fileName = takeFileName filePath fileSize <- liftIO $ fromInteger <$> CF.getFileContentsSize srcFile - when (fileSize > maxFileSizeHard) $ throwError $ INTERNAL "max file size exceeded" + when (fileSize > maxFileSizeHard) $ throwE $ INTERNAL "max file size exceeded" let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing} fileSize' = fromIntegral (B.length fileHdr) + fileSize payloadSize = fileSize' + fileSizeLen + authTagSize chunkSizes <- case redirect of Nothing -> pure $ prepareChunkSizes payloadSize Just _ -> case singleChunkSize payloadSize of - Nothing -> throwError $ INTERNAL "max file size exceeded for redirect" + Nothing -> throwE $ INTERNAL "max file size exceeded for redirect" Just chunkSize -> pure [chunkSize] let encSize = sum $ map fromIntegral chunkSizes void $ liftError (INTERNAL . show) $ encryptFile srcFile fileHdr key nonce fileSize' encSize fsEncPath @@ -432,12 +433,12 @@ runXFTPSndPrepareWorker c Worker {doWork} = do withRetryInterval (riFast ri) $ \_ loop -> do liftIO $ waitForUserNetwork c createWithNextSrv usedSrvs - `catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e + `catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwE e) e where retryLoop loop = atomically (assertAgentForeground c) >> loop createWithNextSrv usedSrvs = do deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId - when deleted $ throwError $ INTERNAL "file deleted, aborting chunk creation" + when deleted $ throwE $ INTERNAL "file deleted, aborting chunk creation" withNextSrv c userId usedSrvs [] $ \srvAuth -> do replica <- agentXFTPNewChunk c ch numRecipients' srvAuth pure (replica, srvAuth) @@ -479,7 +480,7 @@ runXFTPSndWorker c srv Worker {doWork} = do uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica fsFilePath <- lift $ toFSFilePath filePath - unlessM (doesFileExist fsFilePath) $ throwError $ INTERNAL "encrypted file doesn't exist on upload" + unlessM (doesFileExist fsFilePath) $ throwE $ INTERNAL "encrypted file doesn't exist on upload" let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec atomically $ assertAgentForeground c agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec' @@ -499,7 +500,7 @@ runXFTPSndWorker c srv Worker {doWork} = do where addRecipients :: SndFileChunk -> SndFileChunkReplica -> AM SndFileChunkReplica addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {rcvIdsKeys} - | length rcvIdsKeys > numRecipients = throwError $ INTERNAL "too many recipients" + | length rcvIdsKeys > numRecipients = throwE $ INTERNAL "too many recipients" | length rcvIdsKeys == numRecipients = pure cr | otherwise = do let numRecipients' = min (numRecipients - length rcvIdsKeys) maxRecipients @@ -507,22 +508,22 @@ runXFTPSndWorker c srv Worker {doWork} = do cr' <- withStore' c $ \db -> addSndChunkReplicaRecipients db cr $ L.toList rcvIdsKeys' addRecipients ch cr' sndFileToDescrs :: SndFile -> AM (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient]) - sndFileToDescrs SndFile {digest = Nothing} = throwError $ INTERNAL "snd file has no digest" - sndFileToDescrs SndFile {chunks = []} = throwError $ INTERNAL "snd file has no chunks" + sndFileToDescrs SndFile {digest = Nothing} = throwE $ INTERNAL "snd file has no digest" + sndFileToDescrs SndFile {chunks = []} = throwE $ INTERNAL "snd file has no chunks" sndFileToDescrs SndFile {digest = Just digest, key, nonce, chunks = chunks@(fstChunk : _), redirect} = do let chunkSize = FileSize $ sndChunkSize fstChunk size = FileSize $ sum $ map (fromIntegral . sndChunkSize) chunks -- snd description sndDescrChunks <- mapM toSndDescrChunk chunks let fdSnd = FileDescription {party = SFSender, size, digest, key, nonce, chunkSize, chunks = sndDescrChunks, redirect = Nothing} - validFdSnd <- either (throwError . INTERNAL) pure $ validateFileDescription fdSnd + validFdSnd <- either (throwE . INTERNAL) pure $ validateFileDescription fdSnd -- rcv descriptions let fdRcv = FileDescription {party = SFRecipient, size, digest, key, nonce, chunkSize, chunks = [], redirect} fdRcvs = createRcvFileDescriptions fdRcv chunks - validFdRcvs <- either (throwError . INTERNAL) pure $ mapM validateFileDescription fdRcvs + validFdRcvs <- either (throwE . INTERNAL) pure $ mapM validateFileDescription fdRcvs pure (validFdSnd, validFdRcvs) toSndDescrChunk :: SndFileChunk -> AM FileChunk - toSndDescrChunk SndFileChunk {replicas = []} = throwError $ INTERNAL "snd file chunk has no replicas" + toSndDescrChunk SndFileChunk {replicas = []} = throwE $ INTERNAL "snd file chunk has no replicas" toSndDescrChunk ch@SndFileChunk {chunkNo, digest = chDigest, replicas = (SndFileChunkReplica {server, replicaId, replicaKey} : _)} = do let chunkSize = FileSize $ sndChunkSize ch replicas = [FileChunkReplica {server, replicaId, replicaKey}] diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 44d1b596b..445def724 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -138,9 +138,9 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session liftTransportErr (TEHandshake PARSE) . smpDecode =<< liftTransportErr TEBadBlock (C.unPad shsBody) processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do - unless (sessionId == serverSessId) $ throwError $ PCETransportError TEBadSession + unless (sessionId == serverSessId) $ throwE $ PCETransportError TEBadSession case xftpVersionRange `compatibleVRange` serverVRange of - Nothing -> throwError $ PCETransportError TEVersion + Nothing -> throwE $ PCETransportError TEVersion Just (Compatible vr) -> fmap (vr,) . liftTransportErr (TEHandshake BAD_AUTH) $ do let (X.CertificateChain cert, exact) = serverAuth @@ -154,7 +154,7 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session chs' <- liftTransportErr TELargeMsg $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' xftpClientError $ sendRequest c chsReq Nothing - unless (B.null bodyHead) $ throwError $ PCETransportError TEBadBlock + unless (B.null bodyHead) $ throwE $ PCETransportError TEBadBlock liftTransportErr e = liftEitherWith (const $ PCETransportError e) closeXFTPClient :: XFTPClient -> IO () @@ -200,14 +200,14 @@ sendXFTPTransmission XFTPClient {config, thParams, http2Client} t chunkSpec_ = d let req = H.requestStreaming N.methodPost "/" [] streamBody reqTimeout = xftpReqTimeout config $ (\XFTPChunkSpec {chunkSize} -> chunkSize) <$> chunkSpec_ HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- withExceptT xftpClientError . ExceptT $ sendRequest http2Client req (Just reqTimeout) - when (B.length bodyHead /= xftpBlockSize) $ throwError $ PCEResponseError BLOCK + when (B.length bodyHead /= xftpBlockSize) $ throwE $ PCEResponseError BLOCK -- TODO validate that the file ID is the same as in the request? (_, _, (_, _fId, respOrErr)) <- liftEither . first PCEResponseError $ xftpDecodeTransmission thParams bodyHead case respOrErr of Right r -> case protocolError r of - Just e -> throwError $ PCEProtocolError e + Just e -> throwE $ PCEProtocolError e _ -> pure (r, body) - Left e -> throwError $ PCEResponseError e + Left e -> throwE $ PCEResponseError e where streamBody :: (Builder -> IO ()) -> IO () -> IO () streamBody send done = do @@ -250,7 +250,7 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec { let dhSecret = C.dh' sDhKey rpDhKey cbState <- liftEither . first PCECryptoError $ LC.cbInit dhSecret cbNonce let t = chunkTimeout config chunkSize - ExceptT (sequence <$> (t `timeout` (download cbState `catches` errors))) >>= maybe (throwError PCEResponseTimeout) pure + ExceptT (sequence <$> (t `timeout` (download cbState `catches` errors))) >>= maybe (throwE PCEResponseTimeout) pure where errors = [ Handler $ \(_e :: H.HTTP2Error) -> pure $ Left PCENetworkError, @@ -260,8 +260,8 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec { download cbState = runExceptT . withExceptT PCEResponseError $ receiveEncFile chunkPart cbState chunkSpec `catchError` \e -> - whenM (doesFileExist filePath) (removeFile filePath) >> throwError e - _ -> throwError $ PCEResponseError NO_FILE + whenM (doesFileExist filePath) (removeFile filePath) >> throwE e + _ -> throwE $ PCEResponseError NO_FILE (r, _) -> throwE $ unexpectedResponse r xftpReqTimeout :: XFTPClientConfig -> Maybe Word32 -> Int @@ -296,7 +296,7 @@ okResponse = \case -- TODO this currently does not check anything because response size is not set and bodyPart is always Just noFile :: HTTP2Body -> a -> ExceptT XFTPClientError IO a noFile HTTP2Body {bodyPart} a = case bodyPart of - Just _ -> pure a -- throwError $ PCEResponseError HAS_FILE + Just _ -> pure a -- throwE $ PCEResponseError HAS_FILE _ -> pure a -- FACK :: FileCommand Recipient diff --git a/src/Simplex/FileTransfer/Client/Agent.hs b/src/Simplex/FileTransfer/Client/Agent.hs index c17790c2d..86b093ee7 100644 --- a/src/Simplex/FileTransfer/Client/Agent.hs +++ b/src/Simplex/FileTransfer/Client/Agent.hs @@ -11,6 +11,7 @@ import Control.Logger.Simple (logInfo) import Control.Monad import Control.Monad.Except import Control.Monad.Trans (lift) +import Control.Monad.Trans.Except import Data.Bifunctor (first) import qualified Data.ByteString.Char8 as B import Data.Text (Text) @@ -108,7 +109,7 @@ getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do else atomically $ do putTMVar clientVar r TM.delete srv xftpClients - throwError e + throwE e tryConnectAsync :: ME () tryConnectAsync = void . lift . async . runExceptT $ do withRetryInterval (reconnectInterval config) $ \_ loop -> void $ tryConnectClient loop diff --git a/src/Simplex/FileTransfer/Client/Main.hs b/src/Simplex/FileTransfer/Client/Main.hs index 0acc6d3c9..aeac956e6 100644 --- a/src/Simplex/FileTransfer/Client/Main.hs +++ b/src/Simplex/FileTransfer/Client/Main.hs @@ -30,6 +30,7 @@ where import Control.Logger.Simple import Control.Monad import Control.Monad.Except +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (first) @@ -292,7 +293,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re encryptFileForUpload :: TVar ChaChaDRG -> String -> ExceptT CLIError IO (FilePath, FileDescription 'FRecipient, FileDescription 'FSender, [XFTPChunkSpec], Int64) encryptFileForUpload g fileName = do fileSize <- fromInteger <$> getFileSize filePath - when (fileSize > maxFileSize) $ throwError $ CLIError $ "Files bigger than " <> maxFileSizeStr <> " are not supported" + when (fileSize > maxFileSize) $ throwE $ CLIError $ "Files bigger than " <> maxFileSizeStr <> " are not supported" encPath <- getEncPath tempPath "xftp" key <- atomically $ C.randomSbKey g nonce <- atomically $ C.randomCbNonce g @@ -323,7 +324,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re -- upload doesn't allow other requests within the same client until complete (but download does allow). logInfo $ "uploading " <> tshow (length chunks) <> " chunks..." (errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 chunks' . mapM $ runExceptT . uploadFileChunk a) - mapM_ throwError errs + mapM_ throwE errs pure $ map snd (sortOn fst rs) where uploadFileChunk :: XFTPClientAgent -> (Int, XFTPChunkSpec, XFTPServerWithAuth) -> ExceptT CLIError IO (Int, SentFileChunk) @@ -437,12 +438,12 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, srvChunks = groupAllOn srv chunks g <- liftIO C.newRandom (errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 srvChunks $ mapM $ runExceptT . downloadFileChunk g a encPath size downloadedChunks) - mapM_ throwError errs + mapM_ throwE errs let chunkPaths = map snd $ sortOn fst rs encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths - when (encDigest /= unFileDigest digest) $ throwError $ CLIError "File digest mismatch" + when (encDigest /= unFileDigest digest) $ throwE $ CLIError "File digest mismatch" encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths - when (FileSize encSize /= size) $ throwError $ CLIError "File size mismatch" + when (FileSize encSize /= size) $ throwE $ CLIError "File size mismatch" liftIO $ printNoNewLine "Decrypting file..." CryptoFile path _ <- withExceptT cliCryptoError $ decryptChunks encSize chunkPaths key nonce $ fmap CF.plain . getFilePath forM_ chunks $ acknowledgeFileChunk a @@ -464,20 +465,20 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, printProgress "Downloaded" downloaded encSize when verbose $ putStrLn "" pure (chunkNo, chunkPath) - downloadFileChunk _ _ _ _ _ _ = throwError $ CLIError "chunk has no replicas" + downloadFileChunk _ _ _ _ _ _ = throwE $ CLIError "chunk has no replicas" getFilePath :: String -> ExceptT String IO FilePath getFilePath name = case filePath of Just path -> ifM (doesDirectoryExist path) (uniqueCombine path name) $ - ifM (doesFileExist path) (throwError "File already exists") (pure path) + ifM (doesFileExist path) (throwE "File already exists") (pure path) _ -> (`uniqueCombine` name) . ( "Downloads") =<< getHomeDirectory acknowledgeFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO () acknowledgeFileChunk a FileChunk {replicas = replica : _} = do let FileChunkReplica {server, replicaId, replicaKey} = replica c <- withRetry retryCount $ getXFTPServerClient a server withRetry retryCount $ ackXFTPChunk c replicaKey (unChunkReplicaId replicaId) - acknowledgeFileChunk _ _ = throwError $ CLIError "chunk has no replicas" + acknowledgeFileChunk _ _ = throwE $ CLIError "chunk has no replicas" printProgress :: String -> Int64 -> Int64 -> IO () printProgress s part total = printNoNewLine $ s <> " " <> show ((part * 100) `div` total) <> "%" @@ -503,7 +504,7 @@ cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do let FileChunkReplica {server, replicaId, replicaKey} = replica withReconnect a server retryCount $ \c -> deleteXFTPChunk c replicaKey (unChunkReplicaId replicaId) logInfo $ "deleted chunk " <> tshow chunkNo <> " from " <> showServer server - deleteFileChunk _ _ = throwError $ CLIError "chunk has no replicas" + deleteFileChunk _ _ = throwE $ CLIError "chunk has no replicas" cliFileDescrInfo :: InfoOptions -> ExceptT CLIError IO () cliFileDescrInfo InfoOptions {fileDescription} = do @@ -533,7 +534,7 @@ getFileDescription path = getFileDescription' :: FilePartyI p => FilePath -> ExceptT CLIError IO (ValidFileDescription p) getFileDescription' path = getFileDescription path >>= \case - AVFD fd -> either (throwError . CLIError) pure $ checkParty fd + AVFD fd -> either (throwE . CLIError) pure $ checkParty fd singleChunkSize :: Int64 -> Maybe Word32 singleChunkSize size' = @@ -574,13 +575,13 @@ withReconnect a srv n run = withRetry n $ do c <- withRetry n $ getXFTPServerClient a srv withExceptT (CLIError . show) (run c) `catchError` \e -> do liftIO $ closeXFTPServerClient a srv - throwError e + throwE e withRetry :: Show e => Int -> ExceptT e IO a -> ExceptT CLIError IO a withRetry retryCount = withRetry' retryCount . withExceptT (CLIError . show) where withRetry' :: Int -> ExceptT CLIError IO a -> ExceptT CLIError IO a - withRetry' 0 _ = throwError $ CLIError "internal: no retry attempts" + withRetry' 0 _ = throwE $ CLIError "internal: no retry attempts" withRetry' 1 a = a withRetry' n a = a `catchError` \e -> do diff --git a/src/Simplex/FileTransfer/Crypto.hs b/src/Simplex/FileTransfer/Crypto.hs index 547a5675a..72344f3c0 100644 --- a/src/Simplex/FileTransfer/Crypto.hs +++ b/src/Simplex/FileTransfer/Crypto.hs @@ -8,6 +8,7 @@ module Simplex.FileTransfer.Crypto where import Control.Monad import Control.Monad.Except +import Control.Monad.Trans.Except import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (first) import qualified Data.ByteArray as BA @@ -48,17 +49,17 @@ encryptFile srcFile fileHdr key nonce fileSize' encSize encFile = do | otherwise = do let chSize = min len 65536 ch <- liftIO $ get chSize - when (B.length ch /= fromIntegral chSize) $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF" + when (B.length ch /= fromIntegral chSize) $ throwE $ FTCEFileIOError "encrypting file: unexpected EOF" let (ch', sb') = LC.sbEncryptChunk sb ch liftIO $ B.hPut w ch' encryptChunks_ get w (sb', len - chSize) decryptChunks :: Int64 -> [FilePath] -> C.SbKey -> C.CbNonce -> (String -> ExceptT String IO CryptoFile) -> ExceptT FTCryptoError IO CryptoFile -decryptChunks _ [] _ _ _ = throwError $ FTCEInvalidHeader "empty" +decryptChunks _ [] _ _ _ = throwE $ FTCEInvalidHeader "empty" decryptChunks encSize (chPath : chPaths) key nonce getDestFile = case reverse chPaths of [] -> do (!authOk, !f) <- liftEither . first FTCECryptoError . LC.sbDecryptTailTag key nonce (encSize - authTagSize) =<< liftIO (LB.readFile chPath) - unless authOk $ throwError FTCEInvalidAuthTag + unless authOk $ throwE FTCEInvalidAuthTag (FileHeader {fileName}, !f') <- parseFileHeader f destFile <- withExceptT FTCEFileIOError $ getDestFile fileName CF.writeFile destFile f' @@ -73,7 +74,7 @@ decryptChunks encSize (chPath : chPaths) key nonce getDestFile = case reverse ch decryptLastChunk h state' expectedLen unless authOk $ do removeFile path - throwError FTCEInvalidAuthTag + throwE FTCEInvalidAuthTag pure destFile where decryptFirstChunk = do @@ -105,8 +106,8 @@ decryptChunks encSize (chPath : chPaths) key nonce getDestFile = case reverse ch parseFileHeader s = do let (hdrStr, s') = LB.splitAt 1024 s case A.parse smpP $ LB.toStrict hdrStr of - A.Fail _ _ e -> throwError $ FTCEInvalidHeader e - A.Partial _ -> throwError $ FTCEInvalidHeader "incomplete" + A.Fail _ _ e -> throwE $ FTCEInvalidHeader e + A.Partial _ -> throwE $ FTCEInvalidHeader "incomplete" A.Done rest hdr -> pure (hdr, LB.fromStrict rest <> s') readChunks :: [FilePath] -> IO LB.ByteString diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 1ed4894ec..24dcc5e38 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -18,6 +18,7 @@ import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Control.Monad.Trans.Except import Data.Bifunctor (first) import qualified Data.ByteString.Base64.URL as B64 import Data.ByteString.Builder (Builder, byteString) @@ -136,7 +137,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira either sendError pure r where processHello = do - unless (B.null bodyHead) $ throwError HANDSHAKE + unless (B.null bodyHead) $ throwE HANDSHAKE (k, pk) <- atomically . C.generateKeyPair =<< asks random atomically $ TM.insert sessionId (HandshakeSent pk) sessions let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k) @@ -148,11 +149,11 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing processClientHandshake pk = do - unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE + unless (B.length bodyHead == xftpBlockSize) $ throwE HANDSHAKE body <- liftHS $ C.unPad bodyHead XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ smpDecode body kh <- asks serverIdentity - unless (keyHash == kh) $ throwError HANDSHAKE + unless (keyHash == kh) $ throwE HANDSHAKE case compatibleVRange' xftpServerVRange v of Just (Compatible vr) -> do let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} @@ -163,7 +164,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira #endif liftIO . sendResponse $ H.responseNoBody N.ok200 [] pure Nothing - Nothing -> throwError HANDSHAKE + Nothing -> throwE HANDSHAKE sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) sendError err = do runExceptT (encodeXftp err) >>= \case @@ -395,7 +396,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case st <- asks store r <- runExceptT $ do sizes <- asks $ allowedChunkSizes . config - unless (size file `elem` sizes) $ throwError SIZE + unless (size file `elem` sizes) $ throwE SIZE ts <- liftIO getSystemTime -- TODO validate body empty sId <- ExceptT $ addFileRetry st file 3 ts diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 935fa1c42..678d39d52 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -194,7 +194,7 @@ receiveFile_ :: (Handle -> Word32 -> IO (Either XFTPErrorType ())) -> XFTPRcvChu receiveFile_ receive XFTPRcvChunkSpec {filePath, chunkSize, chunkDigest} = do ExceptT $ withFile filePath WriteMode (`receive` chunkSize) digest' <- liftIO $ LC.sha256Hash <$> LB.readFile filePath - when (digest' /= chunkDigest) $ throwError DIGEST + when (digest' /= chunkDigest) $ throwE DIGEST data XFTPErrorType = -- | incorrect block format, encoding or signature size diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 8e9020b7d..07b893b42 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -649,7 +649,7 @@ joinConnAsync c userId corrId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup connId <- withStore c $ \db -> createNewConn db g cData SCMInvitation enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ JOIN enableNtfs (ACR sConnectionMode cReqUri) pqSupport subMode cInfo pure connId - Nothing -> throwError $ AGENT A_VERSION + Nothing -> throwE $ AGENT A_VERSION joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _subMode _cInfo _pqEncryption = throwE $ CMD PROHIBITED "joinConnAsync" @@ -668,7 +668,7 @@ acceptContactAsync' c corrId enableNtfs invId ownConnInfo pqSupport subMode = do withStore' c $ \db -> acceptInvitation db invId ownConnInfo joinConnAsync c userId corrId enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) - throwError err + throwE err _ -> throwE $ CMD PROHIBITED "acceptContactAsync" ackMessageAsync' :: AgentClient -> ACorrId -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AM () @@ -677,7 +677,7 @@ ackMessageAsync' c corrId connId msgId rcptInfo_ = do case cType of SCDuplex -> enqueueAck SCRcv -> enqueueAck - SCSnd -> throwError $ CONN SIMPLEX + SCSnd -> throwE $ CONN SIMPLEX SCContact -> throwE $ CMD PROHIBITED "ackMessageAsync: SCContact" SCNew -> throwE $ CMD PROHIBITED "ackMessageAsync: SCNew" where @@ -740,7 +740,7 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwE e rq' <- withStore c $ \db -> updateNewConnRcv db connId rq lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId when enableNtfs $ do @@ -760,11 +760,11 @@ newConnToJoin c userId connId enableNtfs cReq pqSup = case cReq of CRInvitationUri {} -> lift (compatibleInvitationUri cReq) >>= \case Just (_, (Compatible (CR.E2ERatchetParams v _ _ _)), aVersion) -> create aVersion (Just v) - Nothing -> throwError $ AGENT A_VERSION + Nothing -> throwE $ AGENT A_VERSION CRContactUri {} -> lift (compatibleContactUri cReq) >>= \case Just (_, aVersion) -> create aVersion Nothing - Nothing -> throwError $ AGENT A_VERSION + Nothing -> throwE $ AGENT A_VERSION where create :: Compatible VersionSMPA -> Maybe CR.VersionE2E -> AM ConnId create (Compatible connAgentVersion) e2eV_ = do @@ -796,7 +796,7 @@ startJoinInvitation userId connId enableNtfs cReqUri pqSup = q <- lift $ newSndQueue userId "" qInfo let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} pure (cData, q, rc, e2eSndParams) - Nothing -> throwError $ AGENT A_VERSION + Nothing -> throwE $ AGENT A_VERSION connRequestPQSupport :: AgentClient -> PQSupport -> ConnectionRequestUri c -> IO (Maybe (VersionSMPA, PQSupport)) connRequestPQSupport c pqSup cReq = withAgentEnv' c $ case cReq of @@ -846,14 +846,14 @@ joinConnSrv c userId connId hasNewConn enableNtfs inv@CRInvitationUri {} cInfo p Left e -> do -- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md void $ withStore' c $ \db -> deleteConn db Nothing connId' - throwError e + throwE e joinConnSrv c userId connId hasNewConn enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode srv = lift (compatibleContactUri cReqUri) >>= \case Just (qInfo, vrsn) -> do (connId', cReq) <- newConnSrv c userId connId hasNewConn enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv void $ sendInvitation c userId qInfo vrsn cReq cInfo pure connId' - Nothing -> throwError $ AGENT A_VERSION + Nothing -> throwE $ AGENT A_VERSION joinConnSrvAsync :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM () joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSupport subMode srv = do @@ -899,7 +899,7 @@ acceptContact' c connId enableNtfs invId ownConnInfo pqSupport subMode = withCon withStore' c $ \db -> acceptInvitation db invId ownConnInfo joinConn c userId connId False enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) - throwError err + throwE err _ -> throwE $ CMD PROHIBITED "acceptContact" -- | Reject contact (RJCT command) in Reader monad @@ -916,8 +916,8 @@ subscribeConnection' c connId = toConnResult connId =<< subscribeConnections' c toConnResult :: ConnId -> Map ConnId (Either AgentErrorType ()) -> AM () toConnResult connId rs = case M.lookup connId rs of Just (Right ()) -> when (M.size rs > 1) $ logError $ T.pack $ "too many results " <> show (M.size rs) - Just (Left e) -> throwError e - _ -> throwError $ INTERNAL $ "no result for connection " <> B.unpack connId + Just (Left e) -> throwE e + _ -> throwE $ INTERNAL $ "no result for connection " <> B.unpack connId type QCmdResult = (QueueStatus, Either AgentErrorType ()) @@ -1006,7 +1006,7 @@ getConnectionMessage' c connId = do DuplexConnection _ (rq :| _) _ -> getQueueMessage c rq RcvConnection _ rq -> getQueueMessage c rq ContactConnection _ rq -> getQueueMessage c rq - SndConnection _ _ -> throwError $ CONN SIMPLEX + SndConnection _ _ -> throwE $ CONN SIMPLEX NewConnection _ -> throwE $ CMD PROHIBITED "getConnectionMessage: NewConnection" getNotificationMessage' :: AgentClient -> C.CbNonce -> ByteString -> AM (NotificationInfo, [SMPMsgMeta]) @@ -1146,7 +1146,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do RcvConnection cData rq -> do secure rq senderKey mapM_ (connectReplyQueues c cData ownConnInfo) (L.nonEmpty $ smpReplyQueues senderConf) - _ -> throwError $ INTERNAL $ "incorrect connection type " <> show (internalCmdTag cmd) + _ -> throwE $ INTERNAL $ "incorrect connection type " <> show (internalCmdTag cmd) ICDuplexSecure _rId senderKey -> withServer' . tryWithLock "ICDuplexSecure" . withDuplexConn $ \(DuplexConnection cData (rq :| _) (sq :| _)) -> do secure rq senderKey void $ enqueueMessage c cData sq SMP.MsgFlags {notification = True} HELLO @@ -1182,8 +1182,8 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do tryError (deleteQueue c rq') >>= \case Right () -> finalizeSwitch Left e - | temporaryOrHostError e -> throwError e - | otherwise -> finalizeSwitch >> throwError e + | temporaryOrHostError e -> throwE e + | otherwise -> finalizeSwitch >> throwE e where finalizeSwitch = do withStore' c $ \db -> deleteConnRcvQueue db rq' @@ -1229,7 +1229,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do enqueueMessages :: AgentClient -> ConnData -> NonEmpty SndQueue -> MsgFlags -> AMessage -> AM (AgentMsgId, PQEncryption) enqueueMessages c cData sqs msgFlags aMessage = do - when (ratchetSyncSendProhibited cData) $ throwError $ INTERNAL "enqueueMessages: ratchet is not synchronized" + when (ratchetSyncSendProhibited cData) $ throwE $ INTERNAL "enqueueMessages: ratchet is not synchronized" enqueueMessages' c cData sqs msgFlags aMessage enqueueMessages' :: AgentClient -> ConnData -> NonEmpty SndQueue -> MsgFlags -> AMessage -> AM (AgentMsgId, CR.PQEncryption) @@ -1482,7 +1482,7 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do case conn of DuplexConnection {} -> ack >> sendRcpt conn >> del RcvConnection {} -> ack >> del - SndConnection {} -> throwError $ CONN SIMPLEX + SndConnection {} -> throwE $ CONN SIMPLEX ContactConnection {} -> throwE $ CMD PROHIBITED "ackMessage: ContactConnection" NewConnection _ -> throwE $ CMD PROHIBITED "ackMessage: NewConnection" where @@ -1566,7 +1566,7 @@ abortConnectionSwitch' c connId = let rqs'' = updatedQs rq' rqs' conn' = DuplexConnection cData rqs'' sqs pure $ connectionStats conn' - _ -> throwError $ INTERNAL "won't delete all rcv queues in connection" + _ -> throwE $ INTERNAL "won't delete all rcv queues in connection" | otherwise -> throwE $ CMD PROHIBITED "abortConnectionSwitch: no rcv queues left" _ -> throwE $ CMD PROHIBITED "abortConnectionSwitch: not allowed" _ -> throwE $ CMD PROHIBITED "abortConnectionSwitch: not duplex" @@ -1596,7 +1596,7 @@ ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM () ackQueueMessage c rq srvMsgId = sendAck c rq srvMsgId `catchAgentError` \case SMP _ SMP.NO_MSG -> pure () - e -> throwError e + e -> throwE e -- | Suspend SMP agent connection (OFF command) in Reader monad suspendConnection' :: AgentClient -> ConnId -> AM () @@ -1606,7 +1606,7 @@ suspendConnection' c connId = withConnLock c connId "suspendConnection" $ do DuplexConnection _ rqs _ -> mapM_ (suspendQueue c) rqs RcvConnection _ rq -> suspendQueue c rq ContactConnection _ rq -> suspendQueue c rq - SndConnection _ _ -> throwError $ CONN SIMPLEX + SndConnection _ _ -> throwE $ CONN SIMPLEX NewConnection _ -> throwE $ CMD PROHIBITED "suspendConnection" -- | Delete SMP agent connection (DEL command) in Reader monad @@ -1818,7 +1818,7 @@ registerNtfToken' c suppliedDeviceToken suppliedNtfMode = ns <- asks ntfSupervisor tryReplace ns `catchAgentError` \e -> if temporaryOrHostError e - then throwError e + then throwE e else do withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns @@ -1906,7 +1906,7 @@ toggleConnectionNtfs' c connId enable = do DuplexConnection cData _ _ -> toggle cData RcvConnection cData _ -> toggle cData ContactConnection cData _ -> toggle cData - _ -> throwError $ CONN SIMPLEX + _ -> throwE $ CONN SIMPLEX where toggle :: ConnData -> AM () toggle cData @@ -1926,7 +1926,7 @@ deleteToken_ c tkn@NtfToken {ntfTokenId, ntfTknStatus} = do atomically $ nsUpdateToken ns tkn {ntfTknStatus, ntfTknAction} agentNtfDeleteToken c tknId tkn `catchAgentError` \case NTF _ AUTH -> pure () - e -> throwError e + e -> throwE e withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns @@ -1946,8 +1946,8 @@ withToken c tkn@NtfToken {deviceToken, ntfMode} from_ (toStatus, toAction_) f = withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns void $ registerNtfToken' c deviceToken ntfMode - throwError e - Left e -> throwError e + throwE e + Left e -> throwE e initializeNtfSubs :: AgentClient -> AM () initializeNtfSubs c = sendNtfConnCommands c NSCCreate @@ -2179,7 +2179,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) clientMsg@SMP.ClientMsgEnvelope {cmHeader = SMP.PubHeader phVer e2ePubKey_} <- parseMessage msgBody clientVRange <- asks $ smpClientVRange . config - unless (phVer `isCompatible` clientVRange) . throwError $ AGENT A_VERSION + unless (phVer `isCompatible` clientVRange) . throwE $ AGENT A_VERSION case (e2eDhSecret, e2ePubKey_) of (Nothing, Just e2ePubKey) -> do let e2eDh = C.dh' e2ePubKey e2ePrivKey @@ -2275,7 +2275,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) checkDuplicateHash :: AgentErrorType -> ByteString -> AM () checkDuplicateHash e encryptedMsgHash = unlessM (withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash) $ - throwError e + throwE e updateTotalMsgCount :: STM () updateTotalMsgCount = TM.lookup connId (msgCounts c) >>= \case @@ -2368,7 +2368,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) -- aVRange <- asks $ smpAgentVRange . config -- if agentVersion agentEnvelope `isCompatible` aVRange -- then pure (privHeader, agentEnvelope) - -- else throwError $ AGENT A_VERSION + -- else throwE $ AGENT A_VERSION pure (privHeader, agentEnvelope) parseMessage :: Encoding a => ByteString -> AM a @@ -2381,12 +2381,12 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let ConnData {pqSupport} = toConnData conn' unless (agentVersion `isCompatible` smpAgentVRange && smpClientVersion `isCompatible` smpClientVRange) - (throwError $ AGENT A_VERSION) + (throwE $ AGENT A_VERSION) case status of New -> case (conn', e2eEncryption) of -- party initiating connection (RcvConnection _ _, Just (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _))) -> do - unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwError $ AGENT A_VERSION) + unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwE $ AGENT A_VERSION) (pk1, rcDHRs, pKem) <- withStore c (`getRatchetX3dhKeys` connId) rcParams <- liftError cryptoError $ CR.pqX3dhRcv pk1 rcDHRs pKem e2eSndParams let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eEncryptVRange} @@ -2482,7 +2482,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) qAddMsg :: SMP.MsgId -> NonEmpty (SMPQueueUri, Maybe SndQAddr) -> Connection 'CDuplex -> AM () qAddMsg _ ((_, Nothing) :| _) _ = qError "adding queue without switching is not supported" qAddMsg srvMsgId ((qUri, Just addr) :| _) (DuplexConnection cData' rqs sqs) = do - when (ratchetSyncSendProhibited cData') $ throwError $ AGENT (A_QUEUE "ratchet is not synchronized") + when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized") clientVRange <- asks $ smpClientVRange . config case qUri `compatibleVersion` clientVRange of Just qInfo@(Compatible sqInfo@SMPQueueInfo {queueAddress}) -> @@ -2509,14 +2509,14 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) _ -> qError "absent sender keys" _ -> qError "QADD: won't delete all snd queues in connection" _ -> qError "QADD: replaced queue address is not found in connection" - _ -> throwError $ AGENT A_VERSION + _ -> throwE $ AGENT A_VERSION -- processed by queue recipient qKeyMsg :: SMP.MsgId -> NonEmpty (SMPQueueInfo, SndPublicAuthKey) -> Connection 'CDuplex -> AM () qKeyMsg srvMsgId ((qInfo, senderKey) :| _) conn'@(DuplexConnection cData' rqs _) = do - when (ratchetSyncSendProhibited cData') $ throwError $ AGENT (A_QUEUE "ratchet is not synchronized") + when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized") clientVRange <- asks $ smpClientVRange . config - unless (qInfo `isCompatible` clientVRange) . throwError $ AGENT A_VERSION + unless (qInfo `isCompatible` clientVRange) . throwE $ AGENT A_VERSION case findRQ (smpServer, senderId) rqs of Just rq'@RcvQueue {rcvId, e2ePrivKey = dhPrivKey, smpClientVersion = cVer, status = status'} | status' == New || status' == Confirmed -> do @@ -2536,7 +2536,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) qUseMsg :: SMP.MsgId -> NonEmpty ((SMPServer, SMP.SenderId), Bool) -> Connection 'CDuplex -> AM () -- NOTE: does not yet support the change of the primary status during the rotation qUseMsg srvMsgId ((addr, _primary) :| _) (DuplexConnection cData' rqs sqs) = do - when (ratchetSyncSendProhibited cData') $ throwError $ AGENT (A_QUEUE "ratchet is not synchronized") + when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized") case findQ addr sqs of Just sq'@SndQueue {dbReplaceQueueId = Just replaceQId} -> do case find ((replaceQId ==) . dbQId) sqs of @@ -2555,7 +2555,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) _ -> qError "QUSE: switched queue address not found in connection" qError :: String -> AM a - qError = throwError . AGENT . A_QUEUE + qError = throwE . AGENT . A_QUEUE ereadyMsg :: CR.RatchetX448 -> Connection 'CDuplex -> AM () ereadyMsg rcPrev (DuplexConnection cData'@ConnData {lastExternalSndId} _ sqs) = do @@ -2591,7 +2591,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) newRatchetKey e2eOtherPartyParams@(CR.E2ERatchetParams e2eVersion k1Rcv k2Rcv _) conn'@(DuplexConnection cData'@ConnData {lastExternalSndId, pqSupport} _ sqs) = unlessM ratchetExists $ do AgentConfig {e2eEncryptVRange} <- asks config - unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwError $ AGENT A_VERSION) + unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwE $ AGENT A_VERSION) keys <- getSendRatchetKeys let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eEncryptVRange} initRatchet rcVs keys @@ -2616,7 +2616,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) -- can communicate for other client to reset to RSRequired -- - need to add new AgentMsgEnvelope, AgentMessage, AgentMessageType -- - need to deduplicate on receiving side - throwError $ AGENT (A_CRYPTO RATCHET_SYNC) + throwE $ AGENT (A_CRYPTO RATCHET_SYNC) where sendReplyKey = do g <- asks random @@ -2671,7 +2671,7 @@ checkSQSwchStatus sq@SndQueue {sndSwchStatus} expected = switchStatusError :: (SMPQueueRec q, Show a) => q -> a -> Maybe a -> AM () switchStatusError q expected actual = - throwError . INTERNAL $ + throwE . INTERNAL $ ("unexpected switch status, queueId=" <> show (queueId q)) <> (", expected=" <> show expected) <> (", actual=" <> show actual) @@ -2680,7 +2680,7 @@ connectReplyQueues :: AgentClient -> ConnData -> ConnInfo -> NonEmpty SMPQueueIn connectReplyQueues c cData@ConnData {userId, connId} ownConnInfo (qInfo :| _) = do clientVRange <- asks $ smpClientVRange . config case qInfo `proveCompatible` clientVRange of - Nothing -> throwError $ AGENT A_VERSION + Nothing -> throwE $ AGENT A_VERSION Just qInfo' -> do sq <- lift $ newSndQueue userId connId qInfo' sq' <- withStore c $ \db -> upgradeRcvConnToDuplex db connId sq diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index e18888244..c28c95a0f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -586,7 +586,7 @@ instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do - unlessM (readTVarIO active) . throwError $ INACTIVE + unlessM (readTVarIO active) . throwE $ INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess smpClients ts) >>= either newClient (waitForProtocolClient c tSess smpClients) @@ -597,7 +597,7 @@ getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do - unlessM (readTVarIO active) . throwError $ INACTIVE + unlessM (readTVarIO active) . throwE $ INACTIVE proxySrv <- getNextServer c userId [destSrv] ts <- liftIO getCurrentTime atomically (getClientVar proxySrv ts) >>= \(tSess, auth, v) -> @@ -652,7 +652,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq smpConnectClient :: AgentClient -> SMPTransportSession -> TMap SMPServer ProxiedRelayVar -> SMPClientVar -> AM SMPConnectedClient smpConnectClient c@AgentClient {smpClients, msgQ} tSess@(_, srv, _) prs v = newProtocolClient c tSess smpClients connectClient v - `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e + `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwE e where connectClient :: SMPClientVar -> AM SMPConnectedClient connectClient v' = do @@ -748,7 +748,7 @@ reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, srv, _) = do - unlessM (readTVarIO active) . throwError $ INACTIVE + unlessM (readTVarIO active) . throwE $ INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess ntfClients ts) >>= either @@ -772,7 +772,7 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId, srv, _) = do - unlessM (readTVarIO active) . throwError $ INACTIVE + unlessM (readTVarIO active) . throwE $ INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess xftpClients ts) >>= either @@ -988,7 +988,7 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do logServerError cl e = do logServer "<--" c srv "" $ strEncode e stat cl $ strEncode e - throwError e + throwE e withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do @@ -1007,7 +1007,7 @@ withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do logServerError cl e = do logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ strEncode e stat cl $ strEncode e - throwError e + throwE e withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> AM a) -> AM a withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do @@ -1192,7 +1192,7 @@ runXFTPServerTest c userId (ProtoServerWithAuth srv auth) = do liftError (testErr TSUploadFile) $ X.uploadXFTPChunk xftp spKey sId chunkSpec liftError (testErr TSDownloadFile) $ X.downloadXFTPChunk g xftp rpKey rId $ XFTPRcvChunkSpec rcvPath chSize digest rcvDigest <- liftIO $ C.sha256Hash <$> B.readFile rcvPath - unless (digest == rcvDigest) $ throwError $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST + unless (digest == rcvDigest) $ throwE $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST liftError (testErr TSDeleteFile) $ X.deleteXFTPChunk xftp spKey sId ok <- tcpTimeout xftpNetworkConfig `timeout` X.closeXFTPClient xftp incClientStat c userId xftp "XFTP_TEST" "OK" @@ -1486,7 +1486,7 @@ sendConfirmation c sq@SndQueue {userId, server, sndId, sndPublicKey = Just sndPu let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg sendOrProxySMPMessage c userId server "" Nothing sndId (MsgFlags {notification = True}) msg -sendConfirmation _ _ _ = throwError $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database" +sendConfirmation _ _ _ = throwE $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database" sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM (Maybe SMPServer) sendInvitation c userId (Compatible (SMPQueueInfo v SMPQueueAddress {smpServer, senderId, dhPublicKey})) (Compatible agentVersion) connReq connInfo = do @@ -1657,7 +1657,7 @@ xftpRcvKeys n = do rKeys <- atomically . replicateM n . C.generateAuthKeyPair C.SEd25519 =<< asks random case L.nonEmpty rKeys of Just rKeys' -> pure rKeys' - _ -> throwError $ INTERNAL "non-positive number of recipients" + _ -> throwE $ INTERNAL "non-positive number of recipients" xftpRcvIdsKeys :: NonEmpty ByteString -> NonEmpty C.AAuthKeyPair -> NonEmpty (ChunkReplicaId, C.APrivateAuthKey) xftpRcvIdsKeys rIds rKeys = L.map ChunkReplicaId rIds `L.zip` L.map snd rKeys @@ -1895,7 +1895,7 @@ withUserServers :: forall p a. (ProtocolTypeI p, UserProtocol p) => AgentClient withUserServers c userId action = atomically (TM.lookup userId $ userServers c) >>= \case Just srvs -> action srvs - _ -> throwError $ INTERNAL "unknown userId - no user servers" + _ -> throwE $ INTERNAL "unknown userId - no user servers" withNextSrv :: forall p a. (ProtocolTypeI p, UserProtocol p) => AgentClient -> UserId -> TVar [ProtocolServer p] -> [ProtocolServer p] -> (ProtoServerWithAuth p -> AM a) -> AM a withNextSrv c userId usedSrvs initUsed action = do diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 6c2c5906d..a8b18c5b7 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -225,6 +225,7 @@ import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J import qualified Data.Attoparsec.ByteString.Char8 as A @@ -1045,7 +1046,7 @@ getWorkItem :: Show i => ByteString -> IO (Maybe i) -> (i -> IO (Either StoreErr getWorkItem itemName getId getItem markFailed = runExceptT $ handleErr "getId" getId >>= mapM tryGetItem where - tryGetItem itemId = ExceptT (getItem itemId) `catchStoreErrors` \e -> mark itemId >> throwError e + tryGetItem itemId = ExceptT (getItem itemId) `catchStoreErrors` \e -> mark itemId >> throwE e mark itemId = handleErr ("markFailed ID " <> bshow itemId) $ markFailed itemId catchStoreErrors = catchAllErrors (SEInternal . bshow) -- Errors caught by this function will suspend worker as if there is no more work, diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 56ebf7a3f..e4413d595 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -933,8 +933,8 @@ forwardSMPMessage :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> E forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of - Nothing -> throwError $ PCETransportError TENoServerAuth - Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret + Nothing -> throwE $ PCETransportError TENoServerAuth + Just THAuthClient {sessSecret} -> maybe (throwE $ PCETransportError TENoServerAuth) pure sessSecret nonce <- liftIO . atomically $ C.randomCbNonce g -- wrap let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission} diff --git a/src/Simplex/Messaging/Crypto/File.hs b/src/Simplex/Messaging/Crypto/File.hs index 9608d21b7..3ab491946 100644 --- a/src/Simplex/Messaging/Crypto/File.hs +++ b/src/Simplex/Messaging/Crypto/File.hs @@ -23,6 +23,7 @@ where import Control.Exception import Control.Monad import Control.Monad.Except +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J import qualified Data.ByteArray as BA @@ -56,10 +57,10 @@ readFile (CryptoFile path cfArgs) = do case cfArgs of Just (CFArgs (C.SbKey key) (C.CbNonce nonce)) -> do let len = LB.length s - fromIntegral C.authTagSize - when (len < 0) $ throwError FTCEInvalidFileSize + when (len < 0) $ throwE FTCEInvalidFileSize let (s', tag') = LB.splitAt len s (tag :| cs) <- liftEitherWith FTCECryptoError $ LC.secretBox LC.sbDecryptChunk key nonce s' - unless (BA.constEq (LB.toStrict tag') tag) $ throwError FTCEInvalidAuthTag + unless (BA.constEq (LB.toStrict tag') tag) $ throwE FTCEInvalidAuthTag pure $ LB.fromChunks cs Nothing -> pure s @@ -96,7 +97,7 @@ hGetTag :: CryptoFileHandle -> ExceptT FTCryptoError IO () hGetTag (CFHandle h sb_) = forM_ sb_ $ \sb -> do tag <- liftIO $ B.hGet h C.authTagSize tag' <- LC.sbAuth <$> readTVarIO sb - unless (BA.constEq tag tag') $ throwError FTCEInvalidAuthTag + unless (BA.constEq tag tag') $ throwE FTCEInvalidAuthTag data FTCryptoError = FTCECryptoError C.CryptoError diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index 14f567820..148d931a9 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -447,7 +447,7 @@ pqX3dhRcv rpk1 rpk2 rpKem_ (E2ERatchetParams v sk1 sk2 sKem_) = do Just (PrivateRKParamsProposed ks@(_, pk)) -> do shared <- liftIO $ sntrup761Dec ct pk pure $ Just (ks, RatchetKEMAccepted k' shared ct) - Nothing -> throwError CERatchetKEMState + Nothing -> throwE CERatchetKEMState _ -> pure Nothing -- both parties can send "proposal" in case of ratchet renegotiation pqX3dh :: DhAlgorithm a => (PublicKey a, PublicKey a) -> DhSecret a -> DhSecret a -> DhSecret a -> Maybe RatchetKEMAccepted -> RatchetInitParams diff --git a/src/Simplex/Messaging/Notifications/Server/Push/APNS.hs b/src/Simplex/Messaging/Notifications/Server/Push/APNS.hs index 151f5e044..2632ff4b4 100644 --- a/src/Simplex/Messaging/Notifications/Server/Push/APNS.hs +++ b/src/Simplex/Messaging/Notifications/Server/Push/APNS.hs @@ -15,6 +15,7 @@ import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.Trans.Except import Crypto.Hash.Algorithms (SHA256 (..)) import qualified Crypto.PubKey.ECC.ECDSA as EC import qualified Crypto.PubKey.ECC.Types as ECT @@ -353,18 +354,18 @@ apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {toke | status == Just N.ok200 = pure () | status == Just N.badRequest400 = case reason' of - "BadDeviceToken" -> throwError PPTokenInvalid - "DeviceTokenNotForTopic" -> throwError PPTokenInvalid - "TopicDisallowed" -> throwError PPPermanentError + "BadDeviceToken" -> throwE PPTokenInvalid + "DeviceTokenNotForTopic" -> throwE PPTokenInvalid + "TopicDisallowed" -> throwE PPPermanentError _ -> err status reason' | status == Just N.forbidden403 = case reason' of - "ExpiredProviderToken" -> throwError PPPermanentError -- there should be no point retrying it as the token was refreshed - "InvalidProviderToken" -> throwError PPPermanentError + "ExpiredProviderToken" -> throwE PPPermanentError -- there should be no point retrying it as the token was refreshed + "InvalidProviderToken" -> throwE PPPermanentError _ -> err status reason' - | status == Just N.gone410 = throwError PPTokenInvalid - | status == Just N.serviceUnavailable503 = liftIO (disconnectApnsHTTP2Client c) >> throwError PPRetryLater + | status == Just N.gone410 = throwE PPTokenInvalid + | status == Just N.serviceUnavailable503 = liftIO (disconnectApnsHTTP2Client c) >> throwE PPRetryLater -- Just tooManyRequests429 -> TooManyRequests - too many requests for the same token | otherwise = err status reason' err :: Maybe Status -> Text -> ExceptT PushProviderError IO () - err s r = throwError $ PPResponseError s r + err s r = throwE $ PPResponseError s r liftHTTPS2 a = ExceptT $ first PPConnection <$> a diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index 077ce634e..77a598c5c 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -116,7 +116,7 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do getHandshake th >>= \case NtfClientHandshake {ntfVersion = v, keyHash} | keyHash /= kh -> - throwError $ TEHandshake IDENTITY + throwE $ TEHandshake IDENTITY | otherwise -> case compatibleVRange' ntfVersionRange v of Just (Compatible vr) -> pure $ ntfThHandleServer th v vr pk @@ -128,7 +128,7 @@ ntfClientHandshake c keyHash ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th if sessionId /= sessId - then throwError TEBadSession + then throwE TEBadSession else case ntfVersionRange `compatibleVRange` ntfVRange of Just (Compatible vr) -> do ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do diff --git a/src/Simplex/Messaging/Util.hs b/src/Simplex/Messaging/Util.hs index ef2cc6933..37557cd23 100644 --- a/src/Simplex/Messaging/Util.hs +++ b/src/Simplex/Messaging/Util.hs @@ -7,6 +7,7 @@ import qualified Control.Exception as E import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift +import Control.Monad.Trans.Except import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -114,11 +115,11 @@ catchAllErrors' err action handler = tryAllErrors' err action >>= either handler {-# INLINE catchAllErrors' #-} catchThrow :: MonadUnliftIO m => ExceptT e m a -> (E.SomeException -> e) -> ExceptT e m a -catchThrow action err = catchAllErrors err action throwError +catchThrow action err = catchAllErrors err action throwE {-# INLINE catchThrow #-} allFinally :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> ExceptT e m b -> ExceptT e m a -allFinally err action final = tryAllErrors err action >>= \r -> final >> either throwError pure r +allFinally err action final = tryAllErrors err action >>= \r -> final >> either throwE pure r {-# INLINE allFinally #-} eitherToMaybe :: Either a b -> Maybe b @@ -149,7 +150,7 @@ safeDecodeUtf8 = decodeUtf8With onError onError _ _ = Just '?' timeoutThrow :: MonadUnliftIO m => e -> Int -> ExceptT e m a -> ExceptT e m a -timeoutThrow e ms action = ExceptT (sequence <$> (ms `timeout` runExceptT action)) >>= maybe (throwError e) pure +timeoutThrow e ms action = ExceptT (sequence <$> (ms `timeout` runExceptT action)) >>= maybe (throwE e) pure threadDelay' :: Int64 -> IO () threadDelay' = loop diff --git a/src/Simplex/RemoteControl/Client.hs b/src/Simplex/RemoteControl/Client.hs index 9ef1f820a..a21630454 100644 --- a/src/Simplex/RemoteControl/Client.hs +++ b/src/Simplex/RemoteControl/Client.hs @@ -30,6 +30,7 @@ import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import Data.ByteString (ByteString) @@ -106,9 +107,9 @@ connectRCHost drg pairing@RCHostPairing {caKey, caCert, idPrivKey, knownHost} ct action <- liftIO $ runClient c r hostKeys -- wait for the port to make invitation portNum <- atomically $ readTMVar startedPort - signedInv@RCSignedInvitation {invitation} <- maybe (throwError RCETLSStartFailed) (liftIO . mkInvitation hostKeys address) portNum + signedInv@RCSignedInvitation {invitation} <- maybe (throwE RCETLSStartFailed) (liftIO . mkInvitation hostKeys address) portNum when multicast $ case knownHost of - Nothing -> throwError RCENewController + Nothing -> throwE RCENewController Just KnownHostPairing {hostDhPubKey} -> do ann <- liftIO . async . runExceptT $ announceRC drg 60 idPrivKey hostDhPubKey hostKeys invitation atomically $ putTMVar announcer ann @@ -117,7 +118,7 @@ connectRCHost drg pairing@RCHostPairing {caKey, caCert, idPrivKey, knownHost} ct findCtrlAddress :: ExceptT RCErrorType IO (NonEmpty RCCtrlAddress) findCtrlAddress = do found' <- liftIO $ getLocalAddress rcAddrPrefs_ - maybe (throwError RCENoLocalAddress) pure $ L.nonEmpty found' + maybe (throwE RCENoLocalAddress) pure $ L.nonEmpty found' mkClient :: IO RCHClient_ mkClient = do startedPort <- newEmptyTMVarIO @@ -211,10 +212,10 @@ prepareHostSession let sharedKey = C.dh' dhPubKey dhPrivKey helloBody <- liftEitherWith (const RCEDecrypt) $ C.cbDecrypt sharedKey nonce encBody hostHello@RCHostHello {v, ca, kem = kemPubKey} <- liftEitherWith RCESyntax $ J.eitherDecodeStrict helloBody - unless (ca == tlsHostFingerprint) $ throwError RCEIdentity + unless (ca == tlsHostFingerprint) $ throwE RCEIdentity (kemCiphertext, kemSharedKey) <- liftIO $ sntrup761Enc drg kemPubKey let hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey - unless (isCompatible v supportedRCPVRange) $ throwError RCEVersion + unless (isCompatible v supportedRCPVRange) $ throwE RCEVersion let keys = HostSessKeys {hybridKey, idPrivKey, sessPrivKey} knownHost' <- updateKnownHost ca dhPubKey let ctrlHello = RCCtrlHello {} @@ -227,7 +228,7 @@ prepareHostSession updateKnownHost :: C.KeyHash -> C.PublicKeyX25519 -> ExceptT RCErrorType IO KnownHostPairing updateKnownHost ca hostDhPubKey = case knownHost_ of Just h -> do - unless (hostFingerprint h == tlsHostFingerprint) . throwError $ + unless (hostFingerprint h == tlsHostFingerprint) . throwE $ RCEInternal "TLS host CA is different from host pairing, should be caught in TLS handshake" pure (h :: KnownHostPairing) {hostDhPubKey} Nothing -> pure KnownHostPairing {hostFingerprint = ca, hostDhPubKey} @@ -257,7 +258,7 @@ connectRCCtrl drg (RCVerifiedInvitation inv@RCInvitation {ca, idkey}) pairing_ h pure RCCtrlPairing {caKey, caCert, ctrlFingerprint = ca, idPubKey = idkey, dhPrivKey, prevDhPrivKey = Nothing} updateCtrlPairing :: RCCtrlPairing -> ExceptT RCErrorType IO RCCtrlPairing updateCtrlPairing pairing@RCCtrlPairing {ctrlFingerprint, idPubKey, dhPrivKey = currDhPrivKey} = do - unless (ca == ctrlFingerprint && idPubKey == idkey) $ throwError RCEIdentity + unless (ca == ctrlFingerprint && idPubKey == idkey) $ throwE RCEIdentity (_, dhPrivKey) <- atomically $ C.generateKeyPair drg pure pairing {dhPrivKey, prevDhPrivKey = Just currDhPrivKey} @@ -278,7 +279,7 @@ connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca, clientCredentials <- liftIO (genTLSCredentials drg caKey caCert) >>= \case TLS.Credentials (creds : _) -> pure $ Just creds - _ -> throwError $ RCEInternal "genTLSCredentials must generate credentials" + _ -> throwE $ RCEInternal "genTLSCredentials must generate credentials" let clientConfig = defaultTransportClientConfig {clientCredentials} ExceptT . runTransportClient clientConfig Nothing host (show port) (Just ca) $ \tls@TLS {tlsBuffer, tlsContext} -> runExceptT $ do -- pump socket to detect connection problems @@ -307,7 +308,7 @@ catchRCError = catchAllErrors (RCEException . show) {-# INLINE catchRCError #-} putRCError :: ExceptT RCErrorType IO a -> TMVar (Either RCErrorType b) -> ExceptT RCErrorType IO a -a `putRCError` r = a `catchRCError` \e -> atomically (tryPutTMVar r $ Left e) >> throwError e +a `putRCError` r = a `catchRCError` \e -> atomically (tryPutTMVar r $ Left e) >> throwE e sendRCPacket :: Encoding a => TLS -> a -> ExceptT RCErrorType IO () sendRCPacket tls pkt = do @@ -317,7 +318,7 @@ sendRCPacket tls pkt = do receiveRCPacket :: Encoding a => TLS -> ExceptT RCErrorType IO a receiveRCPacket tls = do b <- liftIO $ cGet tls xrcpBlockSize - when (B.length b /= xrcpBlockSize) $ throwError RCEBlockSize + when (B.length b /= xrcpBlockSize) $ throwE RCEBlockSize b' <- liftEitherWith (const RCEBlockSize) $ C.unPad b liftEitherWith RCESyntax $ smpDecode b' @@ -329,7 +330,7 @@ prepareHostHello hostAppInfo = do logDebug "Preparing session" case compatibleVersion v supportedRCPVRange of - Nothing -> throwError RCEVersion + Nothing -> throwE RCEVersion Just (Compatible v') -> do nonce <- liftIO . atomically $ C.randomCbNonce drg (kemPubKey, kemPrivKey) <- liftIO $ sntrup761Keypair drg @@ -355,7 +356,7 @@ prepareCtrlSession pure CtrlSessKeys {hybridKey, idPubKey, sessPubKey = skey} RCCtrlEncError {nonce, encMessage} -> do message <- liftEitherWith (const RCEDecrypt) $ C.cbDecrypt sharedKey nonce encMessage - throwError $ RCECtrlError $ T.unpack $ safeDecodeUtf8 message + throwE $ RCECtrlError $ T.unpack $ safeDecodeUtf8 message -- * Multicast discovery @@ -382,7 +383,7 @@ discoverRCCtrl subscribers pairings = r@(_, RCVerifiedInvitation RCInvitation {host}) <- findRCCtrlPairing pairings encInvitation case source of SockAddrInet _ ha | THIPv4 (hostAddressToTuple ha) == host -> pure () - _ -> throwError RCEInvitation + _ -> throwE RCEInvitation pure r where loop :: ExceptT RCErrorType IO a -> ExceptT RCErrorType IO a @@ -392,8 +393,8 @@ findRCCtrlPairing :: NonEmpty RCCtrlPairing -> RCEncInvitation -> ExceptT RCErro findRCCtrlPairing pairings RCEncInvitation {dhPubKey, nonce, encInvitation} = do (pairing, signedInvStr) <- liftEither $ decrypt (L.toList pairings) signedInv <- liftEitherWith RCESyntax $ strDecode signedInvStr - inv@(RCVerifiedInvitation RCInvitation {dh = invDh}) <- maybe (throwError RCEInvitation) pure $ verifySignedInvitation signedInv - unless (invDh == dhPubKey) $ throwError RCEInvitation + inv@(RCVerifiedInvitation RCInvitation {dh = invDh}) <- maybe (throwE RCEInvitation) pure $ verifySignedInvitation signedInv + unless (invDh == dhPubKey) $ throwE RCEInvitation pure (pairing, inv) where decrypt :: [RCCtrlPairing] -> Either RCErrorType (RCCtrlPairing, ByteString) @@ -433,7 +434,7 @@ rcEncryptBody drg hybridKey s = do rcDecryptBody :: KEMHybridSecret -> C.CbNonce -> LazyByteString -> ExceptT RCErrorType IO LazyByteString rcDecryptBody hybridKey nonce ct = do let len = LB.length ct - 16 - when (len < 0) $ throwError RCEDecrypt + when (len < 0) $ throwE RCEDecrypt (ok, s) <- liftEitherWith (const RCEDecrypt) $ LC.kcbDecryptTailTag hybridKey nonce len ct - unless ok $ throwError RCEDecrypt + unless ok $ throwE RCEDecrypt pure s diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index c2badea63..25bbdb260 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -2134,7 +2134,7 @@ testSwitchAsync servers = do withB = withAgent 2 agentCfg servers testDB2 withAgent :: HasCallStack => Int -> AgentConfig -> InitialAgentServers -> FilePath -> (HasCallStack => AgentClient -> IO a) -> IO a -withAgent clientId cfg' servers dbPath = bracket (getSMPAgentClient' clientId cfg' servers dbPath) disposeAgentClient +withAgent clientId cfg' servers dbPath = bracket (getSMPAgentClient' clientId cfg' servers dbPath) (\a -> disposeAgentClient a >> threadDelay 100000) sessionSubscribe :: (forall a. (AgentClient -> IO a) -> IO a) -> [ConnId] -> (AgentClient -> ExceptT AgentErrorType IO ()) -> IO () sessionSubscribe withC connIds a = From 3d605310ed1bb4910e4e5f75487277a064e64e66 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 5 Jun 2024 14:34:40 +0100 Subject: [PATCH 090/125] agent: remove protocol encodings and agent TCP server (#1189) * rfc: remove agent protocol encodings * agent: remove protocol encodings and agent TCP server * update * remove unused code * remove * move tests * add delay to tests * stabilize test * test * more delays * reduce delays * enable all tests * delays * style --- package.yaml | 9 - rfcs/2024-06-01-agent-protocol.md | 19 + simplexmq.cabal | 77 -- src/Simplex/FileTransfer/Agent.hs | 4 +- src/Simplex/Messaging/Agent.hs | 90 +- src/Simplex/Messaging/Agent/Client.hs | 49 +- .../Messaging/Agent/NtfSubSupervisor.hs | 4 +- src/Simplex/Messaging/Agent/Protocol.hs | 808 +++--------------- src/Simplex/Messaging/Agent/Server.hs | 85 -- src/Simplex/Messaging/Agent/Store.hs | 11 +- tests/AgentTests.hs | 623 +------------- tests/AgentTests/FunctionalAPITests.hs | 299 +++++-- tests/AgentTests/NotificationTests.hs | 17 +- tests/AgentTests/SQLiteTests.hs | 2 +- tests/CoreTests/ProtocolErrorTests.hs | 111 --- tests/SMPAgentClient.hs | 182 +--- tests/Test.hs | 2 - tests/XFTPAgent.hs | 2 +- 18 files changed, 468 insertions(+), 1926 deletions(-) create mode 100644 rfcs/2024-06-01-agent-protocol.md delete mode 100644 src/Simplex/Messaging/Agent/Server.hs delete mode 100644 tests/CoreTests/ProtocolErrorTests.hs diff --git a/package.yaml b/package.yaml index 662cf8a0a..ef747da0d 100644 --- a/package.yaml +++ b/package.yaml @@ -147,15 +147,6 @@ executables: - -threaded - -rtsopts - smp-agent: - source-dirs: apps/smp-agent - main: Main.hs - dependencies: - - simplexmq - ghc-options: - - -threaded - - -rtsopts - xftp: source-dirs: apps/xftp main: Main.hs diff --git a/rfcs/2024-06-01-agent-protocol.md b/rfcs/2024-06-01-agent-protocol.md new file mode 100644 index 000000000..616aed33f --- /dev/null +++ b/rfcs/2024-06-01-agent-protocol.md @@ -0,0 +1,19 @@ +# Evolving agent API + +## Problem + +Historically, agent API started as a TCP protocol with encoding. We do not use the actual protocol and maintaining the encoding complicates the evolution of the API. + +Currently, I was trying to add ERRS event to combine multiple subscription errors into one to prevent overloading the UI with processing multiple subscription errors (e.g.): + +```haskell +ERRS :: (ConnId, AgentErrorType) -> ACommand Agent AEConn +``` + +This constructor is not possible to encode/parse in a sensible way other than including lengths of errors. + +## Proposal + +Remove commands type and encodings for commands and events. + +Only keep encodings for the commands that are saved to the database: NEW, JOIN, LET, ACK, SWCH, DEL (this one is no longer used but needs to be supported for backwards compatibility). diff --git a/simplexmq.cabal b/simplexmq.cabal index 4974e9f75..bbe7583fa 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -95,7 +95,6 @@ library Simplex.Messaging.Agent.Protocol Simplex.Messaging.Agent.QueryString Simplex.Messaging.Agent.RetryInterval - Simplex.Messaging.Agent.Server Simplex.Messaging.Agent.Store Simplex.Messaging.Agent.Store.SQLite Simplex.Messaging.Agent.Store.SQLite.Common @@ -352,81 +351,6 @@ executable ntf-server , template-haskell ==2.16.* , text >=1.2.3.0 && <1.3 -executable smp-agent - main-is: Main.hs - other-modules: - Paths_simplexmq - hs-source-dirs: - apps/smp-agent - default-extensions: - StrictData - ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts - build-depends: - aeson ==2.2.* - , ansi-terminal >=0.10 && <0.12 - , asn1-encoding ==0.9.* - , asn1-types ==0.3.* - , async ==2.2.* - , attoparsec ==0.14.* - , base >=4.14 && <5 - , base64-bytestring >=1.0 && <1.3 - , case-insensitive ==1.2.* - , composition ==1.0.* - , constraints >=0.12 && <0.14 - , containers ==0.6.* - , crypton ==0.34.* - , crypton-x509 ==1.7.* - , crypton-x509-store ==1.6.* - , crypton-x509-validation ==1.6.* - , cryptostore ==0.3.* - , data-default ==0.7.* - , direct-sqlcipher ==2.3.* - , directory ==1.3.* - , filepath ==1.4.* - , hourglass ==0.2.* - , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 - , ini ==0.4.1 - , iproute ==1.7.* - , iso8601-time ==0.1.* - , memory ==0.18.* - , mtl >=2.3.1 && <3.0 - , network >=3.1.2.7 && <3.2 - , network-info ==0.2.* - , network-transport ==0.5.6 - , network-udp ==0.0.* - , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* - , random >=1.1 && <1.3 - , simple-logger ==0.1.* - , simplexmq - , socks ==0.6.* - , sqlcipher-simple ==0.4.* - , stm ==2.5.* - , temporary ==1.3.* - , time ==1.12.* - , time-manager ==0.0.* - , tls >=1.7.0 && <1.8 - , transformers ==0.6.* - , unliftio ==0.2.* - , unliftio-core ==0.2.* - , websockets ==0.12.* - , yaml ==0.11.* - , zstd ==0.1.3.* - default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON - if impl(ghc >= 9.6.2) - build-depends: - bytestring ==0.11.* - , template-haskell ==2.20.* - , text >=2.0.1 && <2.2 - if impl(ghc < 9.6.2) - build-depends: - bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 - executable smp-server main-is: Main.hs other-modules: @@ -677,7 +601,6 @@ test-suite simplexmq-test CoreTests.CryptoFileTests CoreTests.CryptoTests CoreTests.EncodingTests - CoreTests.ProtocolErrorTests CoreTests.RetryIntervalTests CoreTests.TRcvQueuesTests CoreTests.UtilTests diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index 654a83207..8da29d28b 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -324,8 +324,8 @@ xftpDeleteRcvFiles' c rcvFileEntityIds = do batchFiles :: (DB.Connection -> DBRcvFileId -> IO a) -> [RcvFile] -> AM' [Either AgentErrorType a] batchFiles f rcvFiles = withStoreBatch' c $ \db -> map (\RcvFile {rcvFileId} -> f db rcvFileId) rcvFiles -notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> EntityId -> ACommand 'Agent e -> m () -notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, APC (sAEntity @e) cmd) +notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> EntityId -> AEvent e -> m () +notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, AEvt (sAEntity @e) cmd) xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> AM SndFileId xftpSendFile' c userId file numRecipients = do diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 07b893b42..7bc638496 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -29,10 +29,7 @@ -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md module Simplex.Messaging.Agent - ( -- * queue-based SMP agent - runAgentClient, - - -- * SMP agent functional API + ( -- * SMP agent functional API AgentClient (..), AE, SubscriptionsInfo (..), @@ -185,7 +182,6 @@ import Simplex.RemoteControl.Client import Simplex.RemoteControl.Invitation import Simplex.RemoteControl.Types import System.Mem.Weak (deRefWeak) -import UnliftIO.Async (race_) import UnliftIO.Concurrent (forkFinally, forkIO, killThread, mkWeakThreadId, threadDelay) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -219,7 +215,7 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = run AgentClient {subQ, acThread} name a = a `E.catchAny` \e -> whenM (isJust <$> readTVarIO acThread) $ do logError $ "Agent thread " <> name <> " crashed: " <> tshow e - atomically $ writeTBQueue subQ ("", "", APC SAEConn $ ERR $ CRITICAL True $ show e) + atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ CRITICAL True $ show e) disconnectAgentClient :: AgentClient -> IO () disconnectAgentClient c@AgentClient {agentEnv = Env {ntfSupervisor = ns, xftpAgent = xa}} = do @@ -573,40 +569,6 @@ logConnection c connected = let event = if connected then "connected to" else "disconnected from" in logInfo $ T.unwords ["client", tshow (clientId c), event, "Agent"] --- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's. -runAgentClient :: AgentClient -> AM' () -runAgentClient c = race_ (subscriber c) (client c) -{-# INLINE runAgentClient #-} - -client :: AgentClient -> AM' () -client c@AgentClient {rcvQ, subQ} = forever $ do - (corrId, entId, cmd) <- atomically $ readTBQueue rcvQ - runExceptT (processCommand c (entId, cmd)) - >>= atomically . writeTBQueue subQ . \case - Left e -> (corrId, entId, APC SAEConn $ ERR e) - Right (entId', resp) -> (corrId, entId', resp) - --- | execute any SMP agent command -processCommand :: AgentClient -> (EntityId, APartyCmd 'Client) -> AM (EntityId, APartyCmd 'Agent) -processCommand c (connId, APC e cmd) = - second (APC e) <$> case cmd of - NEW enableNtfs (ACM cMode) pqIK subMode -> second (INV . ACR cMode) <$> newConn c userId connId enableNtfs cMode Nothing pqIK subMode - JOIN enableNtfs (ACR _ cReq) pqEnc subMode connInfo -> (,OK) <$> joinConn c userId connId False enableNtfs cReq connInfo pqEnc subMode - LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK) - ACPT invId pqEnc ownCInfo -> (,OK) <$> acceptContact' c connId True invId ownCInfo pqEnc SMSubscribe - RJCT invId -> rejectContact' c connId invId $> (connId, OK) - SUB -> subscribeConnection' c connId $> (connId, OK) - SEND pqEnc msgFlags msgBody -> (connId,) . uncurry MID <$> sendMessage' c connId pqEnc msgFlags msgBody - ACK msgId rcptInfo_ -> ackMessage' c connId msgId rcptInfo_ $> (connId, OK) - SWCH -> switchConnection' c connId $> (connId, OK) - OFF -> suspendConnection' c connId $> (connId, OK) - DEL -> deleteConnection' c connId $> (connId, OK) - CHK -> (connId,) . STAT <$> getConnectionServers' c connId - where - -- command interface does not support different users - userId :: UserId - userId = 1 - createUser' :: AgentClient -> NonEmpty SMPServerWithAuth -> NonEmpty XFTPServerWithAuth -> AM UserId createUser' c smp xftp = do userId <- withStore' c createUserRecord @@ -623,12 +585,12 @@ deleteUser' c userId delSMPQueues = do where delUser = whenM (withStore' c (`deleteUserWithoutConns` userId)) . atomically $ - writeTBQueue (subQ c) ("", "", APC SAENone $ DEL_USER userId) + writeTBQueue (subQ c) ("", "", AEvt SAENone $ DEL_USER userId) newConnAsync :: ConnectionModeI c => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> CR.InitialKeys -> SubscriptionMode -> AM ConnId newConnAsync c userId corrId enableNtfs cMode pqInitKeys subMode = do connId <- newConnNoQueues c userId "" enableNtfs cMode (CR.connPQEncryption pqInitKeys) - enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ NEW enableNtfs (ACM cMode) pqInitKeys subMode + enqueueCommand c corrId connId Nothing $ AClientCommand $ NEW enableNtfs (ACM cMode) pqInitKeys subMode pure connId newConnNoQueues :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> PQSupport -> AM ConnId @@ -647,7 +609,7 @@ joinConnAsync c userId corrId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) cData = ConnData {userId, connId = "", connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} connId <- withStore c $ \db -> createNewConn db g cData SCMInvitation - enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ JOIN enableNtfs (ACR sConnectionMode cReqUri) pqSupport subMode cInfo + enqueueCommand c corrId connId Nothing $ AClientCommand $ JOIN enableNtfs (ACR sConnectionMode cReqUri) pqSupport subMode cInfo pure connId Nothing -> throwE $ AGENT A_VERSION joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _subMode _cInfo _pqEncryption = @@ -657,7 +619,7 @@ allowConnectionAsync' :: AgentClient -> ACorrId -> ConnId -> ConfirmationId -> C allowConnectionAsync' c corrId connId confId ownConnInfo = withStore c (`getConn` connId) >>= \case SomeConn _ (RcvConnection _ RcvQueue {server}) -> - enqueueCommand c corrId connId (Just server) $ AClientCommand $ APC SAEConn $ LET confId ownConnInfo + enqueueCommand c corrId connId (Just server) $ AClientCommand $ LET confId ownConnInfo _ -> throwE $ CMD PROHIBITED "allowConnectionAsync" acceptContactAsync' :: AgentClient -> ACorrId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId @@ -687,7 +649,7 @@ ackMessageAsync' c corrId connId msgId rcptInfo_ = do RcvMsg {msgType} <- withStore c $ \db -> getRcvMsg db connId mId when (isJust rcptInfo_ && msgType /= AM_A_MSG_) $ throwE $ CMD PROHIBITED "ackMessageAsync: receipt not allowed" (RcvQueue {server}, _) <- withStore c $ \db -> setMsgUserAck db connId mId - enqueueCommand c corrId connId (Just server) . AClientCommand $ APC SAEConn $ ACK msgId rcptInfo_ + enqueueCommand c corrId connId (Just server) . AClientCommand $ ACK msgId rcptInfo_ deleteConnectionAsync' :: AgentClient -> Bool -> ConnId -> AM () deleteConnectionAsync' c waitDelivery connId = deleteConnectionsAsync' c waitDelivery [connId] @@ -717,7 +679,7 @@ switchConnectionAsync' c corrId connId = | otherwise -> do when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "switchConnectionAsync: send prohibited" rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted - enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn SWCH + enqueueCommand c corrId connId Nothing $ AClientCommand SWCH let rqs' = updatedQs rq1 rqs pure . connectionStats $ DuplexConnection cData rqs' sqs _ -> throwE $ CMD PROHIBITED "switchConnectionAsync: not duplex" @@ -984,7 +946,7 @@ subscribeConnections' c connIds = do let actual = M.size rs expected = length connIds when (actual /= expected) . atomically $ - writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL $ "subscribeConnections result size: " <> show actual <> ", expected " <> show expected) + writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ INTERNAL $ "subscribeConnections result size: " <> show actual <> ", expected " <> show expected) resubscribeConnection' :: AgentClient -> ConnId -> AM () resubscribeConnection' c connId = toConnResult connId =<< resubscribeConnections' c [connId] @@ -1114,7 +1076,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do where processCmd :: RetryInterval -> PendingCommand -> AM () processCmd ri PendingCommand {cmdId, corrId, userId, connId, command} = case command of - AClientCommand (APC _ cmd) -> case cmd of + AClientCommand cmd -> case cmd of NEW enableNtfs (ACM cMode) pqEnc subMode -> noServer $ do usedSrvs <- newTVarIO ([] :: [SMPServer]) tryCommand . withNextSrv c userId usedSrvs [] $ \srv -> do @@ -1223,8 +1185,8 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do tryWithLock name = tryCommand . withConnLock c connId name internalErr s = cmdError $ INTERNAL $ s <> ": " <> show (agentCommandTag command) cmdError e = notify (ERR e) >> withStore' c (`deleteCommand` cmdId) - notify :: forall e. AEntityI e => ACommand 'Agent e -> AM () - notify cmd = atomically $ writeTBQueue subQ (corrId, connId, APC (sAEntity @e) cmd) + notify :: forall e. AEntityI e => AEvent e -> AM () + notify cmd = atomically $ writeTBQueue subQ (corrId, connId, AEvt (sAEntity @e) cmd) -- ^ ^ ^ async command processing / enqueueMessages :: AgentClient -> ConnData -> NonEmpty SndQueue -> MsgFlags -> AMessage -> AM (AgentMsgId, PQEncryption) @@ -1460,9 +1422,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork delMsg = delMsgKeep False delMsgKeep :: Bool -> InternalId -> AM () delMsgKeep keepForReceipt msgId = withStore' c $ \db -> deleteSndMsgDelivery db connId sq msgId keepForReceipt - notify :: forall e. AEntityI e => ACommand 'Agent e -> AM () - notify cmd = atomically $ writeTBQueue subQ ("", connId, APC (sAEntity @e) cmd) - notifyDel :: AEntityI e => InternalId -> ACommand 'Agent e -> AM () + notify :: forall e. AEntityI e => AEvent e -> AM () + notify cmd = atomically $ writeTBQueue subQ ("", connId, AEvt (sAEntity @e) cmd) + notifyDel :: AEntityI e => InternalId -> AEvent e -> AM () notifyDel msgId cmd = notify cmd >> delMsg msgId connError msgId = notifyDel msgId . ERR . CONN qError msgId = notifyDel msgId . ERR . AGENT . A_QUEUE @@ -1663,7 +1625,7 @@ prepareDeleteConnections_ getConnections c waitDelivery connIds = do -- ! between completed deletions of connections, and deletions delayed due to wait for delivery (see deleteConn) deliveryTimeout <- if waitDelivery then asks (Just . connDeleteDeliveryTimeout . config) else pure Nothing rs' <- lift $ catMaybes . rights <$> withStoreBatch' c (\db -> map (deleteConn db deliveryTimeout) (M.keys delRs)) - forM_ rs' $ \cId -> notify ("", cId, APC SAEConn DEL_CONN) + forM_ rs' $ \cId -> notify ("", cId, AEvt SAEConn DEL_CONN) pure (errs' <> delRs, rqs, connIds') where rcvQueues :: SomeConn -> Either (Either AgentErrorType ()) [RcvQueue] @@ -1678,7 +1640,7 @@ deleteConnQueues c waitDelivery ntf rqs = do let connIds = M.keys $ M.filter isRight rs deliveryTimeout <- if waitDelivery then asks (Just . connDeleteDeliveryTimeout . config) else pure Nothing rs' <- catMaybes . rights <$> withStoreBatch' c (\db -> map (deleteConn db deliveryTimeout) connIds) - forM_ rs' $ \cId -> notify ("", cId, APC SAEConn DEL_CONN) + forM_ rs' $ \cId -> notify ("", cId, AEvt SAEConn DEL_CONN) pure rs where deleteQueueRecs :: [(RcvQueue, Either AgentErrorType ())] -> AM' [(RcvQueue, Either AgentErrorType ())] @@ -1698,7 +1660,7 @@ deleteConnQueues c waitDelivery ntf rqs = do Left e | temporaryOrHostError e && deleteErrors rq + 1 < maxErrs -> incRcvDeleteErrors db rq $> ((rq, r), Nothing) | otherwise -> deleteConnRcvQueue db rq $> ((rq, Right ()), Just (notifyRQ rq (Just e))) - notifyRQ rq e_ = notify ("", qConnId rq, APC SAEConn $ DEL_RCVQ (qServer rq) (queueId rq) e_) + notifyRQ rq e_ = notify ("", qConnId rq, AEvt SAEConn $ DEL_RCVQ (qServer rq) (queueId rq) e_) notify = when ntf . atomically . writeTBQueue (subQ c) connResults :: [(RcvQueue, Either AgentErrorType ())] -> Map ConnId (Either AgentErrorType ()) connResults = M.map snd . foldl' addResult M.empty @@ -1735,7 +1697,7 @@ deleteConnections_ getConnections ntf waitDelivery c connIds = do let actual = M.size rs expected = length connIds when (actual /= expected) . atomically $ - writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL $ "deleteConnections result size: " <> show actual <> ", expected " <> show expected) + writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ INTERNAL $ "deleteConnections result size: " <> show actual <> ", expected " <> show expected) getConnectionServers' :: AgentClient -> ConnId -> AM ConnectionStats getConnectionServers' c connId = do @@ -1968,7 +1930,7 @@ sendNtfConnCommands c cmd = do Just (ConnData {enableNtfs}, _) -> when enableNtfs . atomically $ writeTBQueue (ntfSubQ ns) (connId, cmd) _ -> - atomically $ writeTBQueue (subQ c) ("", connId, APC SAEConn $ ERR $ INTERNAL "no connection data") + atomically $ writeTBQueue (subQ c) ("", connId, AEvt SAEConn $ ERR $ INTERNAL "no connection data") setNtfServers :: AgentClient -> [NtfServer] -> IO () setNtfServers c = atomically . writeTVar (ntfServers c) @@ -2050,7 +2012,7 @@ cleanupManager c@AgentClient {subQ} = do run SFERR deleteExpiredReplicasForDeletion liftIO $ threadDelay' int where - run :: forall e. AEntityI e => (AgentErrorType -> ACommand 'Agent e) -> AM () -> AM' () + run :: forall e. AEntityI e => (AgentErrorType -> AEvent e) -> AM () -> AM' () run err a = do waitActive . runExceptT $ a `catchAgentError` (notify "" . err) step <- asks $ cleanupStepInterval . config @@ -2097,8 +2059,8 @@ cleanupManager c@AgentClient {subQ} = do deleteExpiredReplicasForDeletion = do rcvFilesTTL <- asks $ rcvFilesTTL . config withStore' c (`deleteDeletedSndChunkReplicasExpired` rcvFilesTTL) - notify :: forall e. AEntityI e => EntityId -> ACommand 'Agent e -> AM () - notify entId cmd = atomically $ writeTBQueue subQ ("", entId, APC (sAEntity @e) cmd) + notify :: forall e. AEntityI e => EntityId -> AEvent e -> AM () + notify entId cmd = atomically $ writeTBQueue subQ ("", entId, AEvt (sAEntity @e) cmd) data ACKd = ACKd | ACKPending @@ -2151,8 +2113,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) atomically . whenM (isPendingSub connId) $ failSubscription c rq e lift $ notifyErr connId e isPendingSub connId = (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId - notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> ACommand 'Agent e -> m () - notify' connId msg = atomically $ writeTBQueue subQ ("", connId, APC (sAEntity @e) msg) + notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> AEvent e -> m () + notify' connId msg = atomically $ writeTBQueue subQ ("", connId, AEvt (sAEntity @e) msg) notifyErr :: ConnId -> SMPClientError -> AM' () notifyErr connId = notify' connId . ERR . protocolClientError SMP (B.unpack $ strEncode srv) processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM () @@ -2343,7 +2305,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) SMP.ERR e -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e r -> unexpected r where - notify :: forall e m. (AEntityI e, MonadIO m) => ACommand 'Agent e -> m () + notify :: forall e m. (AEntityI e, MonadIO m) => AEvent e -> m () notify = notify' connId prohibited :: String -> AM () diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index c28c95a0f..303bb55be 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -273,8 +273,7 @@ type XFTPTransportSession = TransportSession FileResponse data AgentClient = AgentClient { acThread :: TVar (Maybe (Weak ThreadId)), active :: TVar Bool, - rcvQ :: TBQueue (ATransmission 'Client), - subQ :: TBQueue (ATransmission 'Agent), + subQ :: TBQueue ATransmission, msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg), smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), smpClients :: TMap SMPTransportSession SMPClientVar, @@ -373,7 +372,7 @@ getAgentWorker' toW fromW name hasWork c key ws work = do notifyErr err = do let e = either ((", error: " <>) . show) (\_ -> ", no error") e_ msg = "Worker " <> name <> " for " <> show key <> " terminated " <> show (restartCount rc) <> " times" <> e - writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ err msg) + writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err msg) newWorker :: AgentClient -> STM Worker newWorker c = do @@ -449,7 +448,6 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = qSize = tbqSize cfg acThread <- newTVar Nothing active <- newTVar True - rcvQ <- newTBQueue qSize subQ <- newTBQueue qSize msgQ <- newTBQueue qSize smpServers <- newTVar smp @@ -487,7 +485,6 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = AgentClient { acThread, active, - rcvQ, subQ, msgQ, smpServers, @@ -586,7 +583,7 @@ instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do - unlessM (readTVarIO active) . throwE $ INACTIVE + unlessM (readTVarIO active) $ throwE INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess smpClients ts) >>= either newClient (waitForProtocolClient c tSess smpClients) @@ -597,7 +594,7 @@ getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do - unlessM (readTVarIO active) . throwE $ INACTIVE + unlessM (readTVarIO active) $ throwE INACTIVE proxySrv <- getNextServer c userId [destSrv] ts <- liftIO getCurrentTime atomically (getClientVar proxySrv ts) >>= \(tSess, auth, v) -> @@ -633,7 +630,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq liftIO $ incClientStat c userId clnt "PROXY" "OK" pure $ Right sess Left e -> do - liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e + liftIO $ incClientStat c userId clnt "PROXY" $ bshow e atomically $ do unless (serverHostError e) $ do removeSessVar rv destSrv prs @@ -692,8 +689,8 @@ smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess atomically $ mapM_ (releaseGetLock c) qs runReaderT (resubscribeSMPSession c tSess) env - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () - notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) + notifySub :: forall e. AEntityI e => ConnId -> AEvent e -> IO () + notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, AEvt (sAEntity @e) cmd) resubscribeSMPSession :: AgentClient -> SMPTransportSession -> AM' () resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = do @@ -743,12 +740,12 @@ reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do where handleNotify :: AM' () -> AM' () handleNotify = E.handleAny $ notifySub "" . ERR . INTERNAL . show - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> AM' () - notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) + notifySub :: forall e. AEntityI e => ConnId -> AEvent e -> AM' () + notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, AEvt (sAEntity @e) cmd) getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, srv, _) = do - unlessM (readTVarIO active) . throwE $ INACTIVE + unlessM (readTVarIO active) $ throwE INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess ntfClients ts) >>= either @@ -767,12 +764,12 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, clientDisconnected v client = do atomically $ removeSessVar v tSess ntfClients incClientStat c userId client "DISCONNECT" "" - atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent DISCONNECT client) + atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId, srv, _) = do - unlessM (readTVarIO active) . throwE $ INACTIVE + unlessM (readTVarIO active) $ throwE INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess xftpClients ts) >>= either @@ -791,7 +788,7 @@ getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId clientDisconnected v client = do atomically $ removeSessVar v tSess xftpClients incClientStat c userId client "DISCONNECT" "" - atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent DISCONNECT client) + atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv waitForProtocolClient :: @@ -831,10 +828,10 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv <> " (user " <> bshow userId <> maybe "" (" for entity " <>) entityId_ <> ")" atomically $ putTMVar (sessionVar v) (Right client) liftIO $ incClientStat c userId client "CLIENT" "OK" - atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent CONNECT client) + atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent CONNECT client) pure client Left e -> do - liftIO $ incServerStat c userId srv "CLIENT" $ strEncode e + liftIO $ incServerStat c userId srv "CLIENT" $ bshow e ei <- asks $ persistErrorInterval . config if ei == 0 then atomically $ do @@ -845,11 +842,11 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) throwE e -- signal error to caller -hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> Client msg -> ACommand 'Agent 'AENone +hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> AEvent 'AENone) -> Client msg -> AEvent 'AENone hostEvent event = hostEvent' event . protocolClient {-# INLINE hostEvent #-} -hostEvent' :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> ProtoClient msg -> ACommand 'Agent 'AENone +hostEvent' :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> AEvent 'AENone) -> ProtoClient msg -> AEvent 'AENone hostEvent' event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost getClientConfig :: AgentClient -> (AgentConfig -> ProtocolClientConfig v) -> AM' (ProtocolClientConfig v) @@ -986,8 +983,8 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do stat cl = liftIO . incClientStat c userId cl statCmd logServerError :: Client msg -> AgentErrorType -> AM a logServerError cl e = do - logServer "<--" c srv "" $ strEncode e - stat cl $ strEncode e + logServer "<--" c srv "" $ bshow e + stat cl $ bshow e throwE e withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a @@ -1005,8 +1002,8 @@ withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do proxySrv = showServer . protocolClientServer' . protocolClient logServerError :: SMPConnectedClient -> AgentErrorType -> AM a logServerError cl e = do - logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ strEncode e - stat cl $ strEncode e + logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ bshow e + stat cl $ bshow e throwE e withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> AM a) -> AM a @@ -1719,7 +1716,7 @@ withWork c doWork getWork action = Left e -> notifyErr INTERNAL e where noWork = liftIO $ noWorkToDo doWork - notifyErr err e = atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ err $ show e) + notifyErr err e = atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e) noWorkToDo :: TMVar () -> IO () noWorkToDo = void . atomically . tryTakeTMVar @@ -1762,7 +1759,7 @@ suspendOperation c op endedAction = do notifySuspended :: AgentClient -> STM () notifySuspended c = do -- unsafeIOToSTM $ putStrLn "notifySuspended" - writeTBQueue (subQ c) ("", "", APC SAENone SUSPENDED) + writeTBQueue (subQ c) ("", "", AEvt SAENone SUSPENDED) writeTVar (agentState c) ASSuspended endOperation :: AgentClient -> AgentOperation -> STM () -> STM () diff --git a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs index 1e2c7cb00..a239768b0 100644 --- a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs +++ b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs @@ -29,7 +29,7 @@ import Data.Time (UTCTime, addUTCTime, getCurrentTime) import Data.Time.Clock (diffUTCTime) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Protocol (ACommand (..), APartyCmd (..), AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..), SAEntity (..)) +import Simplex.Messaging.Agent.Protocol (AEvent (..), AEvt (..), AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..), SAEntity (..)) import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite @@ -306,7 +306,7 @@ workerInternalError c connId internalErrStr = do -- TODO change error notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m () -notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, APC SAEConn $ ERR $ INTERNAL internalErrStr) +notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, AEvt SAEConn $ ERR $ INTERNAL internalErrStr) {-# INLINE notifyInternalError #-} getNtfToken :: AM' (Maybe NtfToken) diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index e44c2c0f7..447658cfe 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -49,18 +49,15 @@ module Simplex.Messaging.Agent.Protocol -- * SMP agent protocol types ConnInfo, ACommand (..), - APartyCmd (..), + AEvent (..), + AEvt (..), ACommandTag (..), + AEventTag (..), + AEvtTag (..), aCommandTag, - aPartyCmdTag, - ACmd (..), - APartyCmdTag (..), - ACmdTag (..), - AParty (..), + aEventTag, AEntity (..), - SAParty (..), SAEntity (..), - APartyI (..), AEntityI (..), MsgHash, MsgMeta (..), @@ -117,8 +114,6 @@ module Simplex.Messaging.Agent.Protocol AgentCryptoError (..), cryptoErrToSyncState, ATransmission, - ATransmissionOrError, - ARawTransmission, ConnId, RcvFileId, SndFileId, @@ -137,34 +132,21 @@ module Simplex.Messaging.Agent.Protocol serializeCommand, connMode, connMode', - networkCommandP, dbCommandP, - commandP, connModeT, serializeQueueStatus, queueStatusT, agentMessageType, extraSMPServerHosts, updateSMPServerHosts, - checkParty, - - -- * TCP transport functions - tPut, - tGet, - tPutRaw, - tGetRaw, ) where import Control.Applicative (optional, (<|>)) -import Control.Monad (unless) -import Control.Monad.IO.Class -import Control.Monad.Trans.Except import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.TH as J import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A -import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -176,11 +158,9 @@ import Data.Map (Map) import qualified Data.Map as M import Data.Maybe (fromMaybe, isJust) import Data.Text (Text) -import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time.Clock (UTCTime) import Data.Time.Clock.System (SystemTime) -import Data.Time.ISO8601 import Data.Type.Equality import Data.Typeable () import Data.Word (Word16, Word32) @@ -199,7 +179,6 @@ import Simplex.Messaging.Crypto.Ratchet RcvE2ERatchetParams, RcvE2ERatchetParamsUri, SndE2ERatchetParams, - pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn, ) @@ -236,13 +215,11 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..)) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import Simplex.RemoteControl.Types -import Text.Read import UnliftIO.Exception (Exception) -- SMP agent protocol version history: @@ -296,41 +273,13 @@ e2eEncAgentMsgLength v = \case PQSupportOn | v >= pqdrSMPAgentVersion -> 13634 _ -> 15856 --- | Raw (unparsed) SMP agent protocol transmission. -type ARawTransmission = (ByteString, ByteString, ByteString) - --- | Parsed SMP agent protocol transmission. -type ATransmission p = (ACorrId, EntityId, APartyCmd p) - --- | SMP agent protocol transmission or transmission error. -type ATransmissionOrError p = (ACorrId, EntityId, Either AgentErrorType (APartyCmd p)) +-- | SMP agent event +type ATransmission = (ACorrId, EntityId, AEvt) type UserId = Int64 type ACorrId = ByteString --- | SMP agent protocol participants. -data AParty = Agent | Client - deriving (Eq, Show) - --- | Singleton types for SMP agent protocol participants. -data SAParty :: AParty -> Type where - SAgent :: SAParty Agent - SClient :: SAParty Client - -deriving instance Show (SAParty p) - -instance TestEquality SAParty where - testEquality SAgent SAgent = Just Refl - testEquality SClient SClient = Just Refl - testEquality _ _ = Nothing - -class APartyI (p :: AParty) where sAParty :: SAParty p - -instance APartyI Agent where sAParty = SAgent - -instance APartyI Client where sAParty = SClient - data AEntity = AEConn | AERcvFile | AESndFile | AENone deriving (Eq, Show) @@ -359,155 +308,142 @@ instance AEntityI AESndFile where sAEntity = SAESndFile instance AEntityI AENone where sAEntity = SAENone -data ACmd = forall p e. (APartyI p, AEntityI e) => ACmd (SAParty p) (SAEntity e) (ACommand p e) +data AEvt = forall e. AEntityI e => AEvt (SAEntity e) (AEvent e) -deriving instance Show ACmd - -data APartyCmd p = forall e. AEntityI e => APC (SAEntity e) (ACommand p e) - -instance Eq (APartyCmd p) where - APC e cmd == APC e' cmd' = case testEquality e e' of - Just Refl -> cmd == cmd' +instance Eq AEvt where + AEvt e evt == AEvt e' evt' = case testEquality e e' of + Just Refl -> evt == evt' Nothing -> False -deriving instance Show (APartyCmd p) +deriving instance Show AEvt type ConnInfo = ByteString --- | Parameterized type for SMP agent protocol commands and responses from all participants. -data ACommand (p :: AParty) (e :: AEntity) where - NEW :: Bool -> AConnectionMode -> InitialKeys -> SubscriptionMode -> ACommand Client AEConn -- response INV - INV :: AConnectionRequestUri -> ACommand Agent AEConn - JOIN :: Bool -> AConnectionRequestUri -> PQSupport -> SubscriptionMode -> ConnInfo -> ACommand Client AEConn -- response OK - CONF :: ConfirmationId -> PQSupport -> [SMPServer] -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender, [SMPServer] will be empty only in v1 handshake - LET :: ConfirmationId -> ConnInfo -> ACommand Client AEConn -- ConnInfo is from client - REQ :: InvitationId -> PQSupport -> NonEmpty SMPServer -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender - ACPT :: InvitationId -> PQSupport -> ConnInfo -> ACommand Client AEConn -- ConnInfo is from client - RJCT :: InvitationId -> ACommand Client AEConn - INFO :: PQSupport -> ConnInfo -> ACommand Agent AEConn - CON :: PQEncryption -> ACommand Agent AEConn -- notification that connection is established - SUB :: ACommand Client AEConn - END :: ACommand Agent AEConn - CONNECT :: AProtocolType -> TransportHost -> ACommand Agent AENone - DISCONNECT :: AProtocolType -> TransportHost -> ACommand Agent AENone - DOWN :: SMPServer -> [ConnId] -> ACommand Agent AENone - UP :: SMPServer -> [ConnId] -> ACommand Agent AENone - SWITCH :: QueueDirection -> SwitchPhase -> ConnectionStats -> ACommand Agent AEConn - RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> ACommand Agent AEConn - SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn - MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn - SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn - MWARN :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn - MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn - MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn - MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn - MSGNTF :: SMPMsgMeta -> ACommand Agent AEConn - ACK :: AgentMsgId -> Maybe MsgReceiptInfo -> ACommand Client AEConn - RCVD :: MsgMeta -> NonEmpty MsgReceipt -> ACommand Agent AEConn - QCONT :: ACommand Agent AEConn - SWCH :: ACommand Client AEConn - OFF :: ACommand Client AEConn - DEL :: ACommand Client AEConn - DEL_RCVQ :: SMPServer -> SMP.RecipientId -> Maybe AgentErrorType -> ACommand Agent AEConn - DEL_CONN :: ACommand Agent AEConn - DEL_USER :: Int64 -> ACommand Agent AENone - CHK :: ACommand Client AEConn - STAT :: ConnectionStats -> ACommand Agent AEConn - OK :: ACommand Agent AEConn - ERR :: AgentErrorType -> ACommand Agent AEConn - SUSPENDED :: ACommand Agent AENone - -- XFTP commands and responses - RFPROG :: Int64 -> Int64 -> ACommand Agent AERcvFile - RFDONE :: FilePath -> ACommand Agent AERcvFile - RFERR :: AgentErrorType -> ACommand Agent AERcvFile - SFPROG :: Int64 -> Int64 -> ACommand Agent AESndFile - SFDONE :: ValidFileDescription 'FSender -> [ValidFileDescription 'FRecipient] -> ACommand Agent AESndFile - SFERR :: AgentErrorType -> ACommand Agent AESndFile +-- | Parameterized type for SMP agent events +data AEvent (e :: AEntity) where + INV :: AConnectionRequestUri -> AEvent AEConn + CONF :: ConfirmationId -> PQSupport -> [SMPServer] -> ConnInfo -> AEvent AEConn -- ConnInfo is from sender, [SMPServer] will be empty only in v1 handshake + REQ :: InvitationId -> PQSupport -> NonEmpty SMPServer -> ConnInfo -> AEvent AEConn -- ConnInfo is from sender + INFO :: PQSupport -> ConnInfo -> AEvent AEConn + CON :: PQEncryption -> AEvent AEConn -- notification that connection is established + END :: AEvent AEConn + CONNECT :: AProtocolType -> TransportHost -> AEvent AENone + DISCONNECT :: AProtocolType -> TransportHost -> AEvent AENone + DOWN :: SMPServer -> [ConnId] -> AEvent AENone + UP :: SMPServer -> [ConnId] -> AEvent AENone + SWITCH :: QueueDirection -> SwitchPhase -> ConnectionStats -> AEvent AEConn + RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> AEvent AEConn + MID :: AgentMsgId -> PQEncryption -> AEvent AEConn + SENT :: AgentMsgId -> Maybe SMPServer -> AEvent AEConn + MWARN :: AgentMsgId -> AgentErrorType -> AEvent AEConn + MERR :: AgentMsgId -> AgentErrorType -> AEvent AEConn + MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> AEvent AEConn + MSG :: MsgMeta -> MsgFlags -> MsgBody -> AEvent AEConn + MSGNTF :: SMPMsgMeta -> AEvent AEConn + RCVD :: MsgMeta -> NonEmpty MsgReceipt -> AEvent AEConn + QCONT :: AEvent AEConn + DEL_RCVQ :: SMPServer -> SMP.RecipientId -> Maybe AgentErrorType -> AEvent AEConn + DEL_CONN :: AEvent AEConn + DEL_USER :: Int64 -> AEvent AENone + STAT :: ConnectionStats -> AEvent AEConn + OK :: AEvent AEConn + ERR :: AgentErrorType -> AEvent AEConn + SUSPENDED :: AEvent AENone + RFPROG :: Int64 -> Int64 -> AEvent AERcvFile + RFDONE :: FilePath -> AEvent AERcvFile + RFERR :: AgentErrorType -> AEvent AERcvFile + SFPROG :: Int64 -> Int64 -> AEvent AESndFile + SFDONE :: ValidFileDescription 'FSender -> [ValidFileDescription 'FRecipient] -> AEvent AESndFile + SFERR :: AgentErrorType -> AEvent AESndFile -deriving instance Eq (ACommand p e) +deriving instance Eq (AEvent e) -deriving instance Show (ACommand p e) +deriving instance Show (AEvent e) -data ACmdTag = forall p e. (APartyI p, AEntityI e) => ACmdTag (SAParty p) (SAEntity e) (ACommandTag p e) +data AEvtTag = forall e. AEntityI e => AEvtTag (SAEntity e) (AEventTag e) -data APartyCmdTag p = forall e. AEntityI e => APCT (SAEntity e) (ACommandTag p e) - -instance Eq (APartyCmdTag p) where - APCT e cmd == APCT e' cmd' = case testEquality e e' of - Just Refl -> cmd == cmd' +instance Eq AEvtTag where + AEvtTag e evt == AEvtTag e' evt' = case testEquality e e' of + Just Refl -> evt == evt' Nothing -> False -deriving instance Show (APartyCmdTag p) +deriving instance Show AEvtTag -data ACommandTag (p :: AParty) (e :: AEntity) where - NEW_ :: ACommandTag Client AEConn - INV_ :: ACommandTag Agent AEConn - JOIN_ :: ACommandTag Client AEConn - CONF_ :: ACommandTag Agent AEConn - LET_ :: ACommandTag Client AEConn - REQ_ :: ACommandTag Agent AEConn - ACPT_ :: ACommandTag Client AEConn - RJCT_ :: ACommandTag Client AEConn - INFO_ :: ACommandTag Agent AEConn - CON_ :: ACommandTag Agent AEConn - SUB_ :: ACommandTag Client AEConn - END_ :: ACommandTag Agent AEConn - CONNECT_ :: ACommandTag Agent AENone - DISCONNECT_ :: ACommandTag Agent AENone - DOWN_ :: ACommandTag Agent AENone - UP_ :: ACommandTag Agent AENone - SWITCH_ :: ACommandTag Agent AEConn - RSYNC_ :: ACommandTag Agent AEConn - SEND_ :: ACommandTag Client AEConn - MID_ :: ACommandTag Agent AEConn - SENT_ :: ACommandTag Agent AEConn - MWARN_ :: ACommandTag Agent AEConn - MERR_ :: ACommandTag Agent AEConn - MERRS_ :: ACommandTag Agent AEConn - MSG_ :: ACommandTag Agent AEConn - MSGNTF_ :: ACommandTag Agent AEConn - ACK_ :: ACommandTag Client AEConn - RCVD_ :: ACommandTag Agent AEConn - QCONT_ :: ACommandTag Agent AEConn - SWCH_ :: ACommandTag Client AEConn - OFF_ :: ACommandTag Client AEConn - DEL_ :: ACommandTag Client AEConn - DEL_RCVQ_ :: ACommandTag Agent AEConn - DEL_CONN_ :: ACommandTag Agent AEConn - DEL_USER_ :: ACommandTag Agent AENone - CHK_ :: ACommandTag Client AEConn - STAT_ :: ACommandTag Agent AEConn - OK_ :: ACommandTag Agent AEConn - ERR_ :: ACommandTag Agent AEConn - SUSPENDED_ :: ACommandTag Agent AENone +data ACommand + = NEW Bool AConnectionMode InitialKeys SubscriptionMode -- response INV + | JOIN Bool AConnectionRequestUri PQSupport SubscriptionMode ConnInfo + | LET ConfirmationId ConnInfo -- ConnInfo is from client + | ACK AgentMsgId (Maybe MsgReceiptInfo) + | SWCH + | DEL + deriving (Eq, Show) + +data ACommandTag + = NEW_ + | JOIN_ + | LET_ + | ACK_ + | SWCH_ + | DEL_ + deriving (Show) + +data AEventTag (e :: AEntity) where + INV_ :: AEventTag AEConn + CONF_ :: AEventTag AEConn + REQ_ :: AEventTag AEConn + INFO_ :: AEventTag AEConn + CON_ :: AEventTag AEConn + END_ :: AEventTag AEConn + CONNECT_ :: AEventTag AENone + DISCONNECT_ :: AEventTag AENone + DOWN_ :: AEventTag AENone + UP_ :: AEventTag AENone + SWITCH_ :: AEventTag AEConn + RSYNC_ :: AEventTag AEConn + MID_ :: AEventTag AEConn + SENT_ :: AEventTag AEConn + MWARN_ :: AEventTag AEConn + MERR_ :: AEventTag AEConn + MERRS_ :: AEventTag AEConn + MSG_ :: AEventTag AEConn + MSGNTF_ :: AEventTag AEConn + RCVD_ :: AEventTag AEConn + QCONT_ :: AEventTag AEConn + DEL_RCVQ_ :: AEventTag AEConn + DEL_CONN_ :: AEventTag AEConn + DEL_USER_ :: AEventTag AENone + STAT_ :: AEventTag AEConn + OK_ :: AEventTag AEConn + ERR_ :: AEventTag AEConn + SUSPENDED_ :: AEventTag AENone -- XFTP commands and responses - RFDONE_ :: ACommandTag Agent AERcvFile - RFPROG_ :: ACommandTag Agent AERcvFile - RFERR_ :: ACommandTag Agent AERcvFile - SFPROG_ :: ACommandTag Agent AESndFile - SFDONE_ :: ACommandTag Agent AESndFile - SFERR_ :: ACommandTag Agent AESndFile + RFDONE_ :: AEventTag AERcvFile + RFPROG_ :: AEventTag AERcvFile + RFERR_ :: AEventTag AERcvFile + SFPROG_ :: AEventTag AESndFile + SFDONE_ :: AEventTag AESndFile + SFERR_ :: AEventTag AESndFile -deriving instance Eq (ACommandTag p e) +deriving instance Eq (AEventTag e) -deriving instance Show (ACommandTag p e) +deriving instance Show (AEventTag e) -aPartyCmdTag :: APartyCmd p -> APartyCmdTag p -aPartyCmdTag (APC e cmd) = APCT e $ aCommandTag cmd - -aCommandTag :: ACommand p e -> ACommandTag p e +aCommandTag :: ACommand -> ACommandTag aCommandTag = \case NEW {} -> NEW_ - INV _ -> INV_ JOIN {} -> JOIN_ - CONF {} -> CONF_ LET {} -> LET_ + ACK {} -> ACK_ + SWCH -> SWCH_ + DEL -> DEL_ + +aEventTag :: AEvent e -> AEventTag e +aEventTag = \case + INV _ -> INV_ + CONF {} -> CONF_ REQ {} -> REQ_ - ACPT {} -> ACPT_ - RJCT _ -> RJCT_ INFO {} -> INFO_ CON _ -> CON_ - SUB -> SUB_ END -> END_ CONNECT {} -> CONNECT_ DISCONNECT {} -> DISCONNECT_ @@ -515,7 +451,6 @@ aCommandTag = \case UP {} -> UP_ SWITCH {} -> SWITCH_ RSYNC {} -> RSYNC_ - SEND {} -> SEND_ MID {} -> MID_ SENT {} -> SENT_ MWARN {} -> MWARN_ @@ -523,16 +458,11 @@ aCommandTag = \case MERRS {} -> MERRS_ MSG {} -> MSG_ MSGNTF {} -> MSGNTF_ - ACK {} -> ACK_ RCVD {} -> RCVD_ QCONT -> QCONT_ - SWCH -> SWCH_ - OFF -> OFF_ - DEL -> DEL_ DEL_RCVQ {} -> DEL_RCVQ_ DEL_CONN -> DEL_CONN_ DEL_USER _ -> DEL_USER_ - CHK -> CHK_ STAT _ -> STAT_ OK -> OK_ ERR _ -> ERR_ @@ -547,47 +477,9 @@ aCommandTag = \case data QueueDirection = QDRcv | QDSnd deriving (Eq, Show) -instance StrEncoding QueueDirection where - strEncode = \case - QDRcv -> "rcv" - QDSnd -> "snd" - strP = - A.takeTill (== ' ') >>= \case - "rcv" -> pure QDRcv - "snd" -> pure QDSnd - _ -> fail "bad QueueDirection" - -instance ToJSON QueueDirection where - toEncoding = strToJEncoding - toJSON = strToJSON - -instance FromJSON QueueDirection where - parseJSON = strParseJSON "QueueDirection" - data SwitchPhase = SPStarted | SPConfirmed | SPSecured | SPCompleted deriving (Eq, Show) -instance StrEncoding SwitchPhase where - strEncode = \case - SPStarted -> "started" - SPConfirmed -> "confirmed" - SPSecured -> "secured" - SPCompleted -> "completed" - strP = - A.takeTill (== ' ') >>= \case - "started" -> pure SPStarted - "confirmed" -> pure SPConfirmed - "secured" -> pure SPSecured - "completed" -> pure SPCompleted - _ -> fail "bad SwitchPhase" - -instance ToJSON SwitchPhase where - toEncoding = strToJEncoding - toJSON = strToJSON - -instance FromJSON SwitchPhase where - parseJSON = strParseJSON "SwitchPhase" - data RcvSwitchStatus = RSSwitchStarted | RSSendingQADD @@ -688,31 +580,12 @@ data RcvQueueInfo = RcvQueueInfo } deriving (Eq, Show) -instance StrEncoding RcvQueueInfo where - strEncode RcvQueueInfo {rcvServer, rcvSwitchStatus, canAbortSwitch} = - ("srv=" <> strEncode rcvServer) - <> maybe "" (\switch -> ";switch=" <> strEncode switch) rcvSwitchStatus - <> (";can_abort_switch=" <> strEncode canAbortSwitch) - strP = do - rcvServer <- "srv=" *> strP - rcvSwitchStatus <- optional $ ";switch=" *> strP - canAbortSwitch <- ";can_abort_switch=" *> strP - pure RcvQueueInfo {rcvServer, rcvSwitchStatus, canAbortSwitch} - data SndQueueInfo = SndQueueInfo { sndServer :: SMPServer, sndSwitchStatus :: Maybe SndSwitchStatus } deriving (Eq, Show) -instance StrEncoding SndQueueInfo where - strEncode SndQueueInfo {sndServer, sndSwitchStatus} = - "srv=" <> strEncode sndServer <> maybe "" (\switch -> ";switch=" <> strEncode switch) sndSwitchStatus - strP = do - sndServer <- "srv=" *> strP - sndSwitchStatus <- optional $ ";switch=" *> strP - pure SndQueueInfo {sndServer, sndSwitchStatus} - data ConnectionStats = ConnectionStats { connAgentVersion :: VersionSMPA, rcvQueuesInfo :: [RcvQueueInfo], @@ -722,21 +595,6 @@ data ConnectionStats = ConnectionStats } deriving (Eq, Show) -instance StrEncoding ConnectionStats where - strEncode ConnectionStats {connAgentVersion, rcvQueuesInfo, sndQueuesInfo, ratchetSyncState, ratchetSyncSupported} = - ("agent_version=" <> strEncode connAgentVersion) - <> (" rcv=" <> strEncodeList rcvQueuesInfo) - <> (" snd=" <> strEncodeList sndQueuesInfo) - <> (" sync=" <> strEncode ratchetSyncState) - <> (" sync_supported=" <> strEncode ratchetSyncSupported) - strP = do - connAgentVersion <- "agent_version=" *> strP - rcvQueuesInfo <- " rcv=" *> strListP - sndQueuesInfo <- " snd=" *> strListP - ratchetSyncState <- " sync=" *> strP - ratchetSyncSupported <- " sync_supported=" *> strP - pure ConnectionStats {connAgentVersion, rcvQueuesInfo, sndQueuesInfo, ratchetSyncState, ratchetSyncSupported} - data NotificationsMode = NMPeriodic | NMInstant deriving (Eq, Show) @@ -823,27 +681,6 @@ data MsgMeta = MsgMeta } deriving (Eq, Show) -instance StrEncoding MsgMeta where - strEncode MsgMeta {integrity, recipient = (rmId, rTs), broker = (bmId, bTs), sndMsgId, pqEncryption} = - B.unwords - [ strEncode integrity, - "R=" <> bshow rmId <> "," <> showTs rTs, - "B=" <> encode bmId <> "," <> showTs bTs, - "S=" <> bshow sndMsgId, - "PQ=" <> strEncode pqEncryption - ] - where - showTs = B.pack . formatISO8601Millis - strP = do - integrity <- strP - recipient <- " R=" *> partyMeta A.decimal - broker <- " B=" *> partyMeta base64P - sndMsgId <- " S=" *> A.decimal - pqEncryption <- " PQ=" *> strP - pure MsgMeta {integrity, recipient, broker, sndMsgId, pqEncryption} - where - partyMeta idParser = (,) <$> idParser <* A.char ',' <*> tsISO8601P - data SMPConfirmation = SMPConfirmation { -- | sender's public key to use for authentication of sender's commands at the recepient's server senderKey :: SndPublicAuthKey, @@ -1154,14 +991,6 @@ instance Encoding AMessageReceipt where (agentMsgId, msgHash, Large rcptInfo) <- smpP pure AMessageReceipt {agentMsgId, msgHash, rcptInfo} -instance StrEncoding MsgReceipt where - strEncode MsgReceipt {agentMsgId, msgRcptStatus} = - strEncode agentMsgId <> ":" <> strEncode msgRcptStatus - strP = do - agentMsgId <- strP <* A.char ':' - msgRcptStatus <- strP - pure MsgReceipt {agentMsgId, msgRcptStatus} - instance ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where strEncode = \case CRInvitationUri crData e2eParams -> crEncode "invitation" crData (Just e2eParams) @@ -1209,7 +1038,7 @@ connReqUriP overrideScheme = do crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact adjustAgentVRange vr = let v = max duplexHandshakeSMPAgentVersion $ minVersion vr - in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) + in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) instance ConnectionModeI m => FromJSON (ConnectionRequestUri m) where parseJSON = strParseJSON "ConnectionRequestUri" @@ -1563,81 +1392,6 @@ data AgentCryptoError RATCHET_SYNC deriving (Eq, Read, Show, Exception) -instance StrEncoding AgentCryptoError where - strP = - A.takeTill (== ' ') >>= \case - "DECRYPT_AES" -> pure DECRYPT_AES - "DECRYPT_CB" -> pure DECRYPT_CB - "RATCHET_HEADER" -> pure RATCHET_HEADER - "RATCHET_EARLIER" -> RATCHET_EARLIER <$> _strP - "RATCHET_SKIPPED" -> RATCHET_SKIPPED <$> _strP - "RATCHET_SYNC" -> pure RATCHET_SYNC - _ -> fail "AgentCryptoError" - strEncode = \case - DECRYPT_AES -> "DECRYPT_AES" - DECRYPT_CB -> "DECRYPT_CB" - RATCHET_HEADER -> "RATCHET_HEADER" - RATCHET_EARLIER n -> "RATCHET_EARLIER " <> strEncode n - RATCHET_SKIPPED n -> "RATCHET_SKIPPED " <> strEncode n - RATCHET_SYNC -> "RATCHET_SYNC" - -instance StrEncoding AgentErrorType where - strP = - A.takeTill (== ' ') - >>= \case - "CMD" -> CMD <$> (A.space *> parseRead1) <*> (A.space *> textP) - "CONN" -> CONN <$> (A.space *> parseRead1) - "SMP" -> SMP <$> (A.space *> srvP) <*> _strP - "NTF" -> NTF <$> (A.space *> srvP) <*> _strP - "XFTP" -> XFTP <$> (A.space *> srvP) <*> _strP - "PROXY" -> PROXY <$> (A.space *> srvP) <* A.space <*> srvP <*> _strP - "RCP" -> RCP <$> _strP - "BROKER" -> BROKER <$> (A.space *> srvP) <*> _strP - "AGENT" -> AGENT <$> _strP - "INTERNAL" -> INTERNAL <$> (A.space *> textP) - "CRITICAL" -> CRITICAL <$> (A.space *> parseRead1) <*> (A.space *> textP) - "INACTIVE" -> pure INACTIVE - _ -> fail "bad AgentErrorType" - where - srvP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') - strEncode = \case - CMD e cxt -> "CMD " <> bshow e <> " " <> text cxt - CONN e -> "CONN " <> bshow e - SMP srv e -> "SMP " <> text srv <> " " <> strEncode e - NTF srv e -> "NTF " <> text srv <> " " <> strEncode e - XFTP srv e -> "XFTP " <> text srv <> " " <> strEncode e - PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] - RCP e -> "RCP " <> strEncode e - BROKER srv e -> B.unwords ["BROKER", text srv, strEncode e] - AGENT e -> "AGENT " <> strEncode e - INTERNAL e -> "INTERNAL " <> encodeUtf8 (T.pack e) - CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> encodeUtf8 (T.pack e) - INACTIVE -> "INACTIVE" - where - text = encodeUtf8 . T.pack - -instance StrEncoding SMPAgentError where - strP = - A.takeTill (== ' ') - >>= \case - "MESSAGE" -> pure A_MESSAGE - "PROHIBITED" -> A_PROHIBITED <$> (A.space *> textP) - "VERSION" -> pure A_VERSION - "CRYPTO" -> A_CRYPTO <$> _strP - "DUPLICATE" -> pure A_DUPLICATE - "QUEUE" -> A_QUEUE <$> (A.space *> textP) - _ -> fail "bad SMPAgentError" - strEncode = \case - A_MESSAGE -> "MESSAGE" - A_PROHIBITED e -> "PROHIBITED " <> encodeUtf8 (T.pack e) - A_VERSION -> "VERSION" - A_CRYPTO e -> "CRYPTO " <> strEncode e - A_DUPLICATE -> "DUPLICATE" - A_QUEUE e -> "QUEUE " <> encodeUtf8 (T.pack e) - -textP :: Parser String -textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString - cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState cryptoErrToSyncState = \case DECRYPT_AES -> RSAllowed @@ -1647,190 +1401,38 @@ cryptoErrToSyncState = \case RATCHET_SKIPPED _ -> RSRequired RATCHET_SYNC -> RSRequired --- | SMP agent command and response parser for commands passed via network (only parses binary length) -networkCommandP :: Parser ACmd -networkCommandP = commandP A.takeByteString - -- | SMP agent command and response parser for commands stored in db (fully parses binary bodies) -dbCommandP :: Parser ACmd +dbCommandP :: Parser ACommand dbCommandP = commandP $ A.take =<< (A.decimal <* "\n") -instance StrEncoding ACmdTag where - strEncode (ACmdTag _ _ cmd) = strEncode cmd +instance StrEncoding ACommandTag where strP = A.takeTill (== ' ') >>= \case - "NEW" -> t NEW_ - "INV" -> ct INV_ - "JOIN" -> t JOIN_ - "CONF" -> ct CONF_ - "LET" -> t LET_ - "REQ" -> ct REQ_ - "ACPT" -> t ACPT_ - "RJCT" -> t RJCT_ - "INFO" -> ct INFO_ - "CON" -> ct CON_ - "SUB" -> t SUB_ - "END" -> ct END_ - "CONNECT" -> nt CONNECT_ - "DISCONNECT" -> nt DISCONNECT_ - "DOWN" -> nt DOWN_ - "UP" -> nt UP_ - "SWITCH" -> ct SWITCH_ - "RSYNC" -> ct RSYNC_ - "SEND" -> t SEND_ - "MID" -> ct MID_ - "SENT" -> ct SENT_ - "MWARN" -> ct MWARN_ - "MERR" -> ct MERR_ - "MERRS" -> ct MERRS_ - "MSG" -> ct MSG_ - "MSGNTF" -> ct MSGNTF_ - "ACK" -> t ACK_ - "RCVD" -> ct RCVD_ - "QCONT" -> ct QCONT_ - "SWCH" -> t SWCH_ - "OFF" -> t OFF_ - "DEL" -> t DEL_ - "DEL_RCVQ" -> ct DEL_RCVQ_ - "DEL_CONN" -> ct DEL_CONN_ - "DEL_USER" -> nt DEL_USER_ - "CHK" -> t CHK_ - "STAT" -> ct STAT_ - "OK" -> ct OK_ - "ERR" -> ct ERR_ - "SUSPENDED" -> nt SUSPENDED_ - "RFPROG" -> at SAERcvFile RFPROG_ - "RFDONE" -> at SAERcvFile RFDONE_ - "RFERR" -> at SAERcvFile RFERR_ - "SFPROG" -> at SAESndFile SFPROG_ - "SFDONE" -> at SAESndFile SFDONE_ - "SFERR" -> at SAESndFile SFERR_ - _ -> fail "bad ACmdTag" - where - t = pure . ACmdTag SClient SAEConn - at e = pure . ACmdTag SAgent e - ct = at SAEConn - nt = at SAENone - -instance APartyI p => StrEncoding (APartyCmdTag p) where - strEncode (APCT _ cmd) = strEncode cmd - strP = (\(ACmdTag _ e t) -> checkParty $ APCT e t) <$?> strP - -instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where + "NEW" -> pure NEW_ + "JOIN" -> pure JOIN_ + "LET" -> pure LET_ + "ACK" -> pure ACK_ + "SWCH" -> pure SWCH_ + "DEL" -> pure DEL_ + _ -> fail "bad ACommandTag" strEncode = \case NEW_ -> "NEW" - INV_ -> "INV" JOIN_ -> "JOIN" - CONF_ -> "CONF" LET_ -> "LET" - REQ_ -> "REQ" - ACPT_ -> "ACPT" - RJCT_ -> "RJCT" - INFO_ -> "INFO" - CON_ -> "CON" - SUB_ -> "SUB" - END_ -> "END" - CONNECT_ -> "CONNECT" - DISCONNECT_ -> "DISCONNECT" - DOWN_ -> "DOWN" - UP_ -> "UP" - SWITCH_ -> "SWITCH" - RSYNC_ -> "RSYNC" - SEND_ -> "SEND" - MID_ -> "MID" - SENT_ -> "SENT" - MWARN_ -> "MWARN" - MERR_ -> "MERR" - MERRS_ -> "MERRS" - MSG_ -> "MSG" - MSGNTF_ -> "MSGNTF" ACK_ -> "ACK" - RCVD_ -> "RCVD" - QCONT_ -> "QCONT" SWCH_ -> "SWCH" - OFF_ -> "OFF" DEL_ -> "DEL" - DEL_RCVQ_ -> "DEL_RCVQ" - DEL_CONN_ -> "DEL_CONN" - DEL_USER_ -> "DEL_USER" - CHK_ -> "CHK" - STAT_ -> "STAT" - OK_ -> "OK" - ERR_ -> "ERR" - SUSPENDED_ -> "SUSPENDED" - RFPROG_ -> "RFPROG" - RFDONE_ -> "RFDONE" - RFERR_ -> "RFERR" - SFPROG_ -> "SFPROG" - SFDONE_ -> "SFDONE" - SFERR_ -> "SFERR" - strP = (\(APCT _ t) -> checkEntity t) <$?> strP -checkParty :: forall t p p'. (APartyI p, APartyI p') => t p' -> Either String (t p) -checkParty x = case testEquality (sAParty @p) (sAParty @p') of - Just Refl -> Right x - Nothing -> Left "bad party" - -checkEntity :: forall t e e'. (AEntityI e, AEntityI e') => t e' -> Either String (t e) -checkEntity x = case testEquality (sAEntity @e) (sAEntity @e') of - Just Refl -> Right x - Nothing -> Left "bad entity" - --- | SMP agent command and response parser -commandP :: Parser ByteString -> Parser ACmd +commandP :: Parser ByteString -> Parser ACommand commandP binaryP = strP >>= \case - ACmdTag SClient e cmd -> - ACmd SClient e <$> case cmd of - NEW_ -> s (NEW <$> strP_ <*> strP_ <*> pqIKP <*> (strP <|> pure SMP.SMSubscribe)) - JOIN_ -> s (JOIN <$> strP_ <*> strP_ <*> pqSupP <*> (strP_ <|> pure SMP.SMSubscribe) <*> binaryP) - LET_ -> s (LET <$> A.takeTill (== ' ') <* A.space <*> binaryP) - ACPT_ -> s (ACPT <$> A.takeTill (== ' ') <* A.space <*> pqSupP <*> binaryP) - RJCT_ -> s (RJCT <$> A.takeByteString) - SUB_ -> pure SUB - SEND_ -> s (SEND <$> pqEncP <*> smpP <* A.space <*> binaryP) - ACK_ -> s (ACK <$> A.decimal <*> optional (A.space *> binaryP)) - SWCH_ -> pure SWCH - OFF_ -> pure OFF - DEL_ -> pure DEL - CHK_ -> pure CHK - ACmdTag SAgent e cmd -> - ACmd SAgent e <$> case cmd of - INV_ -> s (INV <$> strP) - CONF_ -> s (CONF <$> A.takeTill (== ' ') <* A.space <*> pqSupP <*> strListP <* A.space <*> binaryP) - REQ_ -> s (REQ <$> A.takeTill (== ' ') <* A.space <*> pqSupP <*> strP_ <*> binaryP) - INFO_ -> s (INFO <$> pqSupP <*> binaryP) - CON_ -> s (CON <$> strP) - END_ -> pure END - CONNECT_ -> s (CONNECT <$> strP_ <*> strP) - DISCONNECT_ -> s (DISCONNECT <$> strP_ <*> strP) - DOWN_ -> s (DOWN <$> strP_ <*> connections) - UP_ -> s (UP <$> strP_ <*> connections) - SWITCH_ -> s (SWITCH <$> strP_ <*> strP_ <*> strP) - RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP) - MID_ -> s (MID <$> A.decimal <*> _strP) - SENT_ -> s (SENT <$> A.decimal <*> _strP) - MWARN_ -> s (MWARN <$> A.decimal <* A.space <*> strP) - MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP) - MERRS_ -> s (MERRS <$> strP_ <*> strP) - MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP) - MSGNTF_ -> s (MSGNTF <$> strP) - RCVD_ -> s (RCVD <$> strP <* A.space <*> strP) - QCONT_ -> pure QCONT - DEL_RCVQ_ -> s (DEL_RCVQ <$> strP_ <*> strP_ <*> strP) - DEL_CONN_ -> pure DEL_CONN - DEL_USER_ -> s (DEL_USER <$> strP) - STAT_ -> s (STAT <$> strP) - OK_ -> pure OK - ERR_ -> s (ERR <$> strP) - SUSPENDED_ -> pure SUSPENDED - RFPROG_ -> s (RFPROG <$> A.decimal <* A.space <*> A.decimal) - RFDONE_ -> s (RFDONE <$> strP) - RFERR_ -> s (RFERR <$> strP) - SFPROG_ -> s (SFPROG <$> A.decimal <* A.space <*> A.decimal) - SFDONE_ -> s (sfDone . safeDecodeUtf8 <$?> binaryP) - SFERR_ -> s (SFERR <$> strP) + NEW_ -> s (NEW <$> strP_ <*> strP_ <*> pqIKP <*> (strP <|> pure SMP.SMSubscribe)) + JOIN_ -> s (JOIN <$> strP_ <*> strP_ <*> pqSupP <*> (strP_ <|> pure SMP.SMSubscribe) <*> binaryP) + LET_ -> s (LET <$> A.takeTill (== ' ') <* A.space <*> binaryP) + ACK_ -> s (ACK <$> A.decimal <*> optional (A.space *> binaryP)) + SWCH_ -> pure SWCH + DEL_ -> pure DEL where s :: Parser a -> Parser a s p = A.space *> p @@ -1838,155 +1440,23 @@ commandP binaryP = pqIKP = strP_ <|> pure (IKNoPQ PQSupportOff) pqSupP :: Parser PQSupport pqSupP = strP_ <|> pure PQSupportOff - pqEncP :: Parser PQEncryption - pqEncP = strP_ <|> pure PQEncOff - connections :: Parser [ConnId] - connections = strP `A.sepBy'` A.char ',' - sfDone :: Text -> Either String (ACommand 'Agent 'AESndFile) - sfDone t = - let ds = T.splitOn fdSeparator t - in case ds of - [] -> Left "no sender file description" - sd : rds -> SFDONE <$> strDecode (encodeUtf8 sd) <*> mapM (strDecode . encodeUtf8) rds - -parseCommand :: ByteString -> Either AgentErrorType ACmd -parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX "parseCommand" -- | Serialize SMP agent command. -serializeCommand :: ACommand p e -> ByteString +serializeCommand :: ACommand -> ByteString serializeCommand = \case NEW ntfs cMode pqIK subMode -> s (NEW_, ntfs, cMode, pqIK, subMode) - INV cReq -> s (INV_, cReq) JOIN ntfs cReq pqSup subMode cInfo -> s (JOIN_, ntfs, cReq, pqSup, subMode, Str $ serializeBinary cInfo) - CONF confId pqSup srvs cInfo -> B.unwords [s CONF_, confId, s pqSup, strEncodeList srvs, serializeBinary cInfo] LET confId cInfo -> B.unwords [s LET_, confId, serializeBinary cInfo] - REQ invId pqSup srvs cInfo -> B.unwords [s REQ_, invId, s pqSup, s srvs, serializeBinary cInfo] - ACPT invId pqSup cInfo -> B.unwords [s ACPT_, invId, s pqSup, serializeBinary cInfo] - RJCT invId -> B.unwords [s RJCT_, invId] - INFO pqSup cInfo -> B.unwords [s INFO_, s pqSup, serializeBinary cInfo] - SUB -> s SUB_ - END -> s END_ - CONNECT p h -> s (CONNECT_, p, h) - DISCONNECT p h -> s (DISCONNECT_, p, h) - DOWN srv conns -> B.unwords [s DOWN_, s srv, connections conns] - UP srv conns -> B.unwords [s UP_, s srv, connections conns] - SWITCH dir phase srvs -> s (SWITCH_, dir, phase, srvs) - RSYNC rrState cryptoErr cstats -> s (RSYNC_, rrState, cryptoErr, cstats) - SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody] - MID mId pqEnc -> s (MID_, mId, pqEnc) - SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_) - MWARN mId e -> s (MWARN_, mId, e) - MERR mId e -> s (MERR_, mId, e) - MERRS mIds e -> s (MERRS_, mIds, e) - MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody] - MSGNTF smpMsgMeta -> s (MSGNTF_, smpMsgMeta) ACK mId rcptInfo_ -> s (ACK_, mId) <> maybe "" (B.cons ' ' . serializeBinary) rcptInfo_ - RCVD msgMeta rcpts -> s (RCVD_, msgMeta, rcpts) - QCONT -> s QCONT_ SWCH -> s SWCH_ - OFF -> s OFF_ DEL -> s DEL_ - DEL_RCVQ srv rcvId err_ -> s (DEL_RCVQ_, srv, rcvId, err_) - DEL_CONN -> s DEL_CONN_ - DEL_USER userId -> s (DEL_USER_, userId) - CHK -> s CHK_ - STAT srvs -> s (STAT_, srvs) - CON pqEnc -> s (CON_, pqEnc) - ERR e -> s (ERR_, e) - OK -> s OK_ - SUSPENDED -> s SUSPENDED_ - RFPROG rcvd total -> s (RFPROG_, rcvd, total) - RFDONE fPath -> s (RFDONE_, fPath) - RFERR e -> s (RFERR_, e) - SFPROG sent total -> s (SFPROG_, sent, total) - SFDONE sd rds -> B.unwords [s SFDONE_, serializeBinary (sfDone sd rds)] - SFERR e -> s (SFERR_, e) where s :: StrEncoding a => a -> ByteString s = strEncode - connections :: [ConnId] -> ByteString - connections = B.intercalate "," . map strEncode - sfDone sd rds = B.intercalate fdSeparator $ strEncode sd : map strEncode rds serializeBinary :: ByteString -> ByteString serializeBinary body = bshow (B.length body) <> "\n" <> body --- | Send raw (unparsed) SMP agent protocol transmission to TCP connection. -tPutRaw :: Transport c => c -> ARawTransmission -> IO () -tPutRaw h (corrId, entity, command) = do - putLn h corrId - putLn h entity - putLn h command - --- | Receive raw (unparsed) SMP agent protocol transmission from TCP connection. -tGetRaw :: Transport c => c -> IO ARawTransmission -tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h - --- | Send SMP agent protocol command (or response) to TCP connection. -tPut :: Transport c => c -> ATransmission p -> IO () -tPut h (corrId, connId, APC _ cmd) = - tPutRaw h (corrId, connId, serializeCommand cmd) - --- | Receive client and agent transmissions from TCP connection. -tGet :: forall c p. Transport c => SAParty p -> c -> IO (ATransmissionOrError p) -tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody - where - tParseLoadBody :: ARawTransmission -> IO (ATransmissionOrError p) - tParseLoadBody t@(corrId, entId, command) = do - let cmd = parseCommand command >>= fromParty >>= tConnId t - fullCmd <- either (return . Left) cmdWithMsgBody cmd - return (corrId, entId, fullCmd) - - fromParty :: ACmd -> Either AgentErrorType (APartyCmd p) - fromParty (ACmd (p :: p1) e cmd) = case testEquality party p of - Just Refl -> Right $ APC e cmd - _ -> Left $ CMD PROHIBITED "fromParty" - - tConnId :: ARawTransmission -> APartyCmd p -> Either AgentErrorType (APartyCmd p) - tConnId (_, entId, _) (APC e cmd) = - APC e <$> case cmd of - -- NEW, JOIN and ACPT have optional connection ID - NEW {} -> Right cmd - JOIN {} -> Right cmd - ACPT {} -> Right cmd - -- ERROR response does not always have connection ID - ERR _ -> Right cmd - CONNECT {} -> Right cmd - DISCONNECT {} -> Right cmd - DOWN {} -> Right cmd - UP {} -> Right cmd - SUSPENDED {} -> Right cmd - -- other responses must have connection ID - _ - | B.null entId -> Left $ CMD NO_CONN "tConnId" - | otherwise -> Right cmd - - cmdWithMsgBody :: APartyCmd p -> IO (Either AgentErrorType (APartyCmd p)) - cmdWithMsgBody (APC e cmd) = - APC e <$$> case cmd of - SEND pqEnc msgFlags body -> SEND pqEnc msgFlags <$$> getBody body - MSG msgMeta msgFlags body -> MSG msgMeta msgFlags <$$> getBody body - JOIN ntfs qUri pqSup subMode cInfo -> JOIN ntfs qUri pqSup subMode <$$> getBody cInfo - CONF confId pqSup srvs cInfo -> CONF confId pqSup srvs <$$> getBody cInfo - LET confId cInfo -> LET confId <$$> getBody cInfo - REQ invId pqSup srvs cInfo -> REQ invId pqSup srvs <$$> getBody cInfo - ACPT invId pqSup cInfo -> ACPT invId pqSup <$$> getBody cInfo - INFO pqSup cInfo -> INFO pqSup <$$> getBody cInfo - _ -> pure $ Right cmd - - getBody :: ByteString -> IO (Either AgentErrorType ByteString) - getBody binary = - case B.unpack binary of - ':' : body -> return . Right $ B.pack body - str -> case readMaybe str :: Maybe Int of - Just size -> runExceptT $ do - body <- liftIO $ cGet h size - unless (B.length body == size) $ throwE $ CMD SIZE "getBody" - s <- liftIO $ getLn h - unless (B.null s) $ throwE $ CMD SIZE "getBody" - pure body - Nothing -> pure . Left $ CMD SYNTAX "getBody" - $(J.deriveJSON defaultJSON ''RcvQueueInfo) $(J.deriveJSON defaultJSON ''SndQueueInfo) @@ -2006,3 +1476,7 @@ $(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError) $(J.deriveJSON (sumTypeJSON id) ''SMPAgentError) $(J.deriveJSON (sumTypeJSON id) ''AgentErrorType) + +$(J.deriveJSON (enumJSON $ dropPrefix "QD") ''QueueDirection) + +$(J.deriveJSON (enumJSON $ dropPrefix "SP") ''SwitchPhase) diff --git a/src/Simplex/Messaging/Agent/Server.hs b/src/Simplex/Messaging/Agent/Server.hs deleted file mode 100644 index da87fde11..000000000 --- a/src/Simplex/Messaging/Agent/Server.hs +++ /dev/null @@ -1,85 +0,0 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} - -module Simplex.Messaging.Agent.Server - ( -- * SMP agent over TCP - runSMPAgent, - runSMPAgentBlocking, - ) -where - -import Control.Logger.Simple (logInfo) -import Control.Monad -import Control.Monad.Reader -import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Char8 as B -import Data.Text.Encoding (decodeUtf8) -import Network.Socket (ServiceName) -import Simplex.Messaging.Agent -import Simplex.Messaging.Agent.Client (newAgentClient) -import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) -import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), simplexMQVersion) -import Simplex.Messaging.Transport.Server (defaultTransportServerConfig, loadTLSServerParams, runTransportServer) -import Simplex.Messaging.Util (bshow) -import UnliftIO.Async (race_) -import qualified UnliftIO.Exception as E -import UnliftIO.STM - --- | Runs an SMP agent as a TCP service using passed configuration. --- --- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs -runSMPAgent :: ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> IO () -runSMPAgent t cfg initServers store = - runSMPAgentBlocking t cfg initServers store 0 =<< newEmptyTMVarIO - --- | Runs an SMP agent as a TCP service using passed configuration with signalling. --- --- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True) --- and when it is disconnected from the TCP socket once the server thread is killed (False). -runSMPAgentBlocking :: ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> Int -> TMVar Bool -> IO () -runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store initClientId started = - case tcpPort of - Just port -> newSMPAgentEnv cfg store >>= smpAgent t port - Nothing -> E.throwIO $ userError "no agent port" - where - smpAgent :: forall c. Transport c => TProxy c -> ServiceName -> Env -> IO () - smpAgent _ port env = do - -- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation - tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile Nothing - clientId <- newTVarIO initClientId - runTransportServer started port tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do - putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion - cId <- atomically $ stateTVar clientId $ \i -> (i + 1, i + 1) - c <- atomically $ newAgentClient cId initServers env - logConnection c True - race_ (connectClient h c) (runAgentClient c `runReaderT` env) - `E.finally` (disconnectAgentClient c) - -connectClient :: Transport c => c -> AgentClient -> IO () -connectClient h c = race_ (send h c) (receive h c) - -receive :: forall c. Transport c => c -> AgentClient -> IO () -receive h c@AgentClient {rcvQ, subQ} = forever $ do - (corrId, entId, cmdOrErr) <- tGet SClient h - case cmdOrErr of - Right cmd -> write rcvQ (corrId, entId, cmd) - Left e -> write subQ (corrId, entId, APC SAEConn $ ERR e) - where - write :: TBQueue (ATransmission p) -> ATransmission p -> IO () - write q t = do - logClient c "-->" t - atomically $ writeTBQueue q t - -send :: Transport c => c -> AgentClient -> IO () -send h c@AgentClient {subQ} = forever $ do - t <- atomically $ readTBQueue subQ - tPut h t - logClient c "<--" t - -logClient :: AgentClient -> ByteString -> ATransmission a -> IO () -logClient AgentClient {clientId} dir (corrId, connId, APC _ cmd) = do - logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd] diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index b3decd8f0..807ca223a 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -47,7 +47,6 @@ import Simplex.Messaging.Protocol VersionSMPC, ) import qualified Simplex.Messaging.Protocol as SMP -import Simplex.Messaging.Util ((<$?>)) -- * Queue types @@ -344,20 +343,20 @@ instance StrEncoding AgentCmdType where _ -> fail "bad AgentCmdType" data AgentCommand - = AClientCommand (APartyCmd 'Client) + = AClientCommand ACommand | AInternalCommand InternalCommand instance StrEncoding AgentCommand where strEncode = \case - AClientCommand (APC _ cmd) -> strEncode (ACClient, Str $ serializeCommand cmd) + AClientCommand cmd -> strEncode (ACClient, Str $ serializeCommand cmd) AInternalCommand cmd -> strEncode (ACInternal, cmd) strP = strP_ >>= \case - ACClient -> AClientCommand <$> ((\(ACmd _ e cmd) -> checkParty $ APC e cmd) <$?> dbCommandP) + ACClient -> AClientCommand <$> dbCommandP ACInternal -> AInternalCommand <$> strP data AgentCommandTag - = AClientCommandTag (APartyCmdTag 'Client) + = AClientCommandTag ACommandTag | AInternalCommandTag InternalCommandTag deriving (Show) @@ -436,7 +435,7 @@ instance StrEncoding InternalCommandTag where agentCommandTag :: AgentCommand -> AgentCommandTag agentCommandTag = \case - AClientCommand cmd -> AClientCommandTag $ aPartyCmdTag cmd + AClientCommand cmd -> AClientCommandTag $ aCommandTag cmd AInternalCommand cmd -> AInternalCommandTag $ internalCmdTag cmd internalCmdTag :: InternalCommand -> InternalCommandTag diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index b200c3933..c9e11f296 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -12,32 +12,12 @@ module AgentTests (agentTests) where import AgentTests.ConnectionRequestTests import AgentTests.DoubleRatchetTests (doubleRatchetTests) -import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg', pattern SENT) +import AgentTests.FunctionalAPITests (functionalAPITests) import AgentTests.MigrationTests (migrationTests) import AgentTests.NotificationTests (notificationTests) import AgentTests.SQLiteTests (storeTests) -import Control.Concurrent -import Control.Monad (forM_, when) -import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Char8 as B -import Data.Maybe (fromJust) -import Data.Type.Equality -import GHC.Stack (withFrozenCallStack) -import Network.HTTP.Types (urlEncode) -import SMPAgentClient -import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) -import Simplex.Messaging.Agent.Protocol hiding (CONF, INFO, MID, REQ, SENT) -import qualified Simplex.Messaging.Agent.Protocol as A -import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) -import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (ErrorType (..)) -import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) -import Simplex.Messaging.Util (bshow) -import System.Directory (removeFile) -import System.Timeout +import Simplex.Messaging.Transport (ATransport (..)) import Test.Hspec -import Util agentTests :: ATransport -> Spec agentTests (ATransport t) = do @@ -47,602 +27,3 @@ agentTests (ATransport t) = do describe "Notification tests" $ notificationTests (ATransport t) describe "SQLite store" storeTests describe "Migration tests" migrationTests - describe "SMP agent protocol syntax" $ syntaxTests t - describe "Establishing duplex connection (via agent protocol)" $ do - skip "These tests are disabled because the agent does not work correctly with multiple connected TCP clients" $ - describe "one agent" $ do - it "should connect via one server and one agent" $ do - smpAgentTest2_1_1 $ testDuplexConnection t - it "should connect via one server and one agent (random IDs)" $ do - smpAgentTest2_1_1 $ testDuplexConnRandomIds t - it "should connect via one server and 2 agents" $ do - smpAgentTest2_2_1 $ testDuplexConnection t - it "should connect via one server and 2 agents (random IDs)" $ do - smpAgentTest2_2_1 $ testDuplexConnRandomIds t - describe "should connect via 2 servers and 2 agents" $ do - pqMatrix2 t smpAgentTest2_2_2 testDuplexConnection' - describe "should connect via 2 servers and 2 agents (random IDs)" $ do - pqMatrix2 t smpAgentTest2_2_2 testDuplexConnRandomIds' - describe "Establishing connections via `contact connection`" $ do - describe "should connect via contact connection with one server and 3 agents" $ do - pqMatrix3 t smpAgentTest3 testContactConnection - describe "should connect via contact connection with one server and 2 agents (random IDs)" $ do - pqMatrix2NoInv t smpAgentTest2_2_1 testContactConnRandomIds - it "should support rejecting contact request" $ do - smpAgentTest2_2_1 $ testRejectContactRequest t - describe "Connection subscriptions" $ do - it "should connect via one server and one agent" $ do - smpAgentTest3_1_1 $ testSubscription t - it "should send notifications to client when server disconnects" $ do - smpAgentServerTest $ testSubscrNotification t - describe "Message delivery and server reconnection" $ do - describe "should deliver messages after losing server connection and re-connecting" $ - pqMatrix2 t smpAgentTest2_2_2_needs_server testMsgDeliveryServerRestart - it "should connect to the server when server goes up if it initially was down" $ do - smpAgentTestN [] $ testServerConnectionAfterError t - it "should deliver pending messages after agent restarting" $ do - smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t - it "should concurrently deliver messages to connections without blocking" $ do - smpAgentTest2_2_1 $ testConcurrentMsgDelivery t - it "should deliver messages if one of connections has quota exceeded" $ do - smpAgentTest2_2_1 $ testMsgDeliveryQuotaExceeded t - it "should resume delivering messages after exceeding quota once all messages are received" $ do - smpAgentTest2_2_1 $ testResumeDeliveryQuotaExceeded t - -type AEntityTransmission p e = (ACorrId, ConnId, ACommand p e) - -type AEntityTransmissionOrError p e = (ACorrId, ConnId, Either AgentErrorType (ACommand p e)) - -tGetAgent :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AEConn) -tGetAgent = tGetAgent' True - -tGetAgent' :: forall c e. (Transport c, AEntityI e) => Bool -> c -> IO (AEntityTransmissionOrError 'Agent e) -tGetAgent' skipErr h = do - (corrId, connId, cmdOrErr) <- pGetAgent skipErr h - case cmdOrErr of - Right (APC e cmd) -> case testEquality e (sAEntity @e) of - Just Refl -> pure (corrId, connId, Right cmd) - _ -> error $ "unexpected command " <> show cmd - Left err -> pure (corrId, connId, Left err) - -pGetAgent :: forall c. Transport c => Bool -> c -> IO (ATransmissionOrError 'Agent) -pGetAgent skipErr h = do - (corrId, connId, cmdOrErr) <- tGet SAgent h - case cmdOrErr of - Right (APC _ CONNECT {}) -> pGetAgent skipErr h - Right (APC _ DISCONNECT {}) -> pGetAgent skipErr h - Right (APC _ (ERR (BROKER _ NETWORK))) | skipErr -> pGetAgent skipErr h - cmd -> pure (corrId, connId, cmd) - --- | receive message to handle `h` -(<#:) :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AEConn) -(<#:) = tGetAgent - -(<#:?) :: Transport c => c -> IO (ATransmissionOrError 'Agent) -(<#:?) = pGetAgent True - -(<#:.) :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AENone) -(<#:.) = tGetAgent' True - --- | send transmission `t` to handle `h` and get response -(#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (AEntityTransmissionOrError 'Agent 'AEConn) -h #: t = tPutRaw h t >> (<#:) h - -(#:!) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (AEntityTransmissionOrError 'Agent 'AEConn) -h #:! t = tPutRaw h t >> tGetAgent' False h - --- | action and expected response --- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r` -(#>) :: IO (AEntityTransmissionOrError 'Agent 'AEConn) -> AEntityTransmission 'Agent 'AEConn -> Expectation -action #> (corrId, connId, cmd) = withFrozenCallStack $ action `shouldReturn` (corrId, connId, Right cmd) - --- | action and predicate for the response --- `h #:t =#> p` is the test that sends `t` to `h` and validates the response using `p` -(=#>) :: IO (AEntityTransmissionOrError 'Agent 'AEConn) -> (AEntityTransmission 'Agent 'AEConn -> Bool) -> Expectation -action =#> p = withFrozenCallStack $ action >>= (`shouldSatisfy` p . correctTransmission) - -pattern MID :: AgentMsgId -> ACommand 'Agent 'AEConn -pattern MID msgId = A.MID msgId PQEncOn - -correctTransmission :: (ACorrId, ConnId, Either AgentErrorType cmd) -> (ACorrId, ConnId, cmd) -correctTransmission (corrId, connId, cmdOrErr) = case cmdOrErr of - Right cmd -> (corrId, connId, cmd) - Left e -> error $ show e - --- | receive message to handle `h` and validate that it is the expected one -(<#) :: (HasCallStack, Transport c) => c -> AEntityTransmission 'Agent 'AEConn -> Expectation -h <# (corrId, connId, cmd) = timeout 5000000 (h <#:) `shouldReturn` Just (corrId, connId, Right cmd) - -(<#.) :: (HasCallStack, Transport c) => c -> AEntityTransmission 'Agent 'AENone -> Expectation -h <#. (corrId, connId, cmd) = timeout 5000000 (h <#:.) `shouldReturn` Just (corrId, connId, Right cmd) - --- | receive message to handle `h` and validate it using predicate `p` -(<#=) :: (HasCallStack, Transport c) => c -> (AEntityTransmission 'Agent 'AEConn -> Bool) -> Expectation -h <#= p = timeout 5000000 (h <#:) >>= (`shouldSatisfy` p . correctTransmission . fromJust) - -(<#=?) :: (HasCallStack, Transport c) => c -> (ATransmission 'Agent -> Bool) -> Expectation -h <#=? p = timeout 5000000 (h <#:?) >>= (`shouldSatisfy` p . correctTransmission . fromJust) - --- | test that nothing is delivered to handle `h` during 10ms -(#:#) :: Transport c => c -> String -> Expectation -h #:# err = tryGet `shouldReturn` () - where - tryGet = - 10000 `timeout` tGetAgent h >>= \case - Just _ -> error err - _ -> return () - -type PQMatrix2 c = - HasCallStack => - TProxy c -> - (HasCallStack => (c -> c -> IO ()) -> Expectation) -> - (HasCallStack => (c, InitialKeys) -> (c, PQSupport) -> IO ()) -> - Spec - -pqMatrix2 :: PQMatrix2 c -pqMatrix2 = pqMatrix2_ True - -pqMatrix2NoInv :: PQMatrix2 c -pqMatrix2NoInv = pqMatrix2_ False - -pqMatrix2_ :: Bool -> PQMatrix2 c -pqMatrix2_ pqInv _ smpTest test = do - it "dh/dh handshake" $ smpTest $ \a b -> test (a, IKPQOff) (b, PQSupportOff) - it "dh/pq handshake" $ smpTest $ \a b -> test (a, IKPQOff) (b, PQSupportOn) - it "pq/dh handshake" $ smpTest $ \a b -> test (a, IKPQOn) (b, PQSupportOff) - it "pq/pq handshake" $ smpTest $ \a b -> test (a, IKPQOn) (b, PQSupportOn) - when pqInv $ do - it "pq-inv/dh handshake" $ smpTest $ \a b -> test (a, IKUsePQ) (b, PQSupportOff) - it "pq-inv/pq handshake" $ smpTest $ \a b -> test (a, IKUsePQ) (b, PQSupportOn) - -pqMatrix3 :: - HasCallStack => - TProxy c -> - (HasCallStack => (c -> c -> c -> IO ()) -> Expectation) -> - (HasCallStack => (c, InitialKeys) -> (c, PQSupport) -> (c, PQSupport) -> IO ()) -> - Spec -pqMatrix3 _ smpTest test = do - it "dh" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOff) (c, PQSupportOff) - it "dh/dh/pq" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOff) (c, PQSupportOn) - it "dh/pq/dh" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOn) (c, PQSupportOff) - it "dh/pq/pq" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOn) (c, PQSupportOn) - it "pq/dh/dh" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOff) (c, PQSupportOff) - it "pq/dh/pq" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOff) (c, PQSupportOn) - it "pq/pq/dh" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOn) (c, PQSupportOff) - it "pq" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOn) (c, PQSupportOn) - -testDuplexConnection :: (HasCallStack, Transport c) => TProxy c -> c -> c -> IO () -testDuplexConnection _ alice bob = testDuplexConnection' (alice, IKPQOn) (bob, PQSupportOn) - -testDuplexConnection' :: (HasCallStack, Transport c) => (c, InitialKeys) -> (c, PQSupport) -> IO () -testDuplexConnection' (alice, aPQ) (bob, bPQ) = do - let pq = pqConnectionMode aPQ bPQ - ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW T INV" <> pqConnModeStr aPQ <> " subscribe") - let cReq' = strEncode cReq - bob #: ("11", "alice", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "bob", Right (A.CONF confId pqSup' _ "bob's connInfo")) <- (alice <#:) - pqSup' `shouldBe` CR.connPQEncryption aPQ - alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) - bob <# ("", "alice", A.INFO bPQ "alice's connInfo") - bob <# ("", "alice", CON pq) - alice <# ("", "bob", CON pq) - -- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4 - alice #: ("3", "bob", "SEND F :hello") #> ("3", "bob", A.MID 4 pq) - alice <# ("", "bob", SENT 4) - bob <#= \case ("", "alice", Msg' 4 pq' "hello") -> pq == pq'; _ -> False - bob #: ("12", "alice", "ACK 4") #> ("12", "alice", OK) - alice #: ("4", "bob", "SEND F :how are you?") #> ("4", "bob", A.MID 5 pq) - alice <# ("", "bob", SENT 5) - bob <#= \case ("", "alice", Msg' 5 pq' "how are you?") -> pq == pq'; _ -> False - bob #: ("13", "alice", "ACK 5") #> ("13", "alice", OK) - bob #: ("14", "alice", "SEND F 9\nhello too") #> ("14", "alice", A.MID 6 pq) - bob <# ("", "alice", SENT 6) - alice <#= \case ("", "bob", Msg' 6 pq' "hello too") -> pq == pq'; _ -> False - alice #: ("3a", "bob", "ACK 6") #> ("3a", "bob", OK) - bob #: ("15", "alice", "SEND F 9\nmessage 1") #> ("15", "alice", A.MID 7 pq) - bob <# ("", "alice", SENT 7) - alice <#= \case ("", "bob", Msg' 7 pq' "message 1") -> pq == pq'; _ -> False - alice #: ("4a", "bob", "ACK 7") #> ("4a", "bob", OK) - alice #: ("5", "bob", "OFF") #> ("5", "bob", OK) - bob #: ("17", "alice", "SEND F 9\nmessage 3") #> ("17", "alice", A.MID 8 pq) - bob <#= \case ("", "alice", MERR 8 (SMP _ AUTH)) -> True; _ -> False - alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) - alice #:# "nothing else should be delivered to alice" - -testDuplexConnRandomIds :: (HasCallStack, Transport c) => TProxy c -> c -> c -> IO () -testDuplexConnRandomIds _ alice bob = testDuplexConnRandomIds' (alice, IKPQOn) (bob, PQSupportOn) - -testDuplexConnRandomIds' :: (HasCallStack, Transport c) => (c, InitialKeys) -> (c, PQSupport) -> IO () -testDuplexConnRandomIds' (alice, aPQ) (bob, bPQ) = do - let pq = pqConnectionMode aPQ bPQ - ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW T INV" <> pqConnModeStr aPQ <> " subscribe") - let cReq' = strEncode cReq - ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") - ("", bobConn', Right (A.CONF confId pqSup' _ "bob's connInfo")) <- (alice <#:) - pqSup' `shouldBe` CR.connPQEncryption aPQ - bobConn' `shouldBe` bobConn - alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False - bob <# ("", aliceConn, A.INFO bPQ "alice's connInfo") - bob <# ("", aliceConn, CON pq) - alice <# ("", bobConn, CON pq) - alice #: ("2", bobConn, "SEND F :hello") #> ("2", bobConn, A.MID 4 pq) - alice <# ("", bobConn, SENT 4) - bob <#= \case ("", c, Msg' 4 pq' "hello") -> c == aliceConn && pq == pq'; _ -> False - bob #: ("12", aliceConn, "ACK 4") #> ("12", aliceConn, OK) - alice #: ("3", bobConn, "SEND F :how are you?") #> ("3", bobConn, A.MID 5 pq) - alice <# ("", bobConn, SENT 5) - bob <#= \case ("", c, Msg' 5 pq' "how are you?") -> c == aliceConn && pq == pq'; _ -> False - bob #: ("13", aliceConn, "ACK 5") #> ("13", aliceConn, OK) - bob #: ("14", aliceConn, "SEND F 9\nhello too") #> ("14", aliceConn, A.MID 6 pq) - bob <# ("", aliceConn, SENT 6) - alice <#= \case ("", c, Msg' 6 pq' "hello too") -> c == bobConn && pq == pq'; _ -> False - alice #: ("3a", bobConn, "ACK 6") #> ("3a", bobConn, OK) - bob #: ("15", aliceConn, "SEND F 9\nmessage 1") #> ("15", aliceConn, A.MID 7 pq) - bob <# ("", aliceConn, SENT 7) - alice <#= \case ("", c, Msg' 7 pq' "message 1") -> c == bobConn && pq == pq'; _ -> False - alice #: ("4a", bobConn, "ACK 7") #> ("4a", bobConn, OK) - alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK) - bob #: ("17", aliceConn, "SEND F 9\nmessage 3") #> ("17", aliceConn, A.MID 8 pq) - bob <#= \case ("", cId, MERR 8 (SMP _ AUTH)) -> cId == aliceConn; _ -> False - alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) - alice #:# "nothing else should be delivered to alice" - -testContactConnection :: Transport c => (c, InitialKeys) -> (c, PQSupport) -> (c, PQSupport) -> IO () -testContactConnection (alice, aPQ) (bob, bPQ) (tom, tPQ) = do - ("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW T CON" <> pqConnModeStr aPQ <> " subscribe") - let cReq' = strEncode cReq - abPQ = pqConnectionMode aPQ bPQ - aPQMode = CR.connPQEncryption aPQ - - bob #: ("11", "alice", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "alice_contact", Right (A.REQ aInvId PQSupportOn _ "bob's connInfo")) <- (alice <#:) - alice #: ("2", "bob", "ACPT " <> aInvId <> enableKEMStr aPQMode <> " 16\nalice's connInfo") #> ("2", "bob", OK) - ("", "alice", Right (A.CONF bConfId pqSup'' _ "alice's connInfo")) <- (bob <#:) - pqSup'' `shouldBe` bPQ - bob #: ("12", "alice", "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK) - alice <# ("", "bob", A.INFO (CR.connPQEncryption aPQ) "bob's connInfo 2") - alice <# ("", "bob", CON abPQ) - bob <# ("", "alice", CON abPQ) - alice #: ("3", "bob", "SEND F :hi") #> ("3", "bob", A.MID 4 abPQ) - alice <# ("", "bob", SENT 4) - bob <#= \case ("", "alice", Msg' 4 pq' "hi") -> pq' == abPQ; _ -> False - bob #: ("13", "alice", "ACK 4") #> ("13", "alice", OK) - - let atPQ = pqConnectionMode aPQ tPQ - tom #: ("21", "alice", "JOIN T " <> cReq' <> enableKEMStr tPQ <> " subscribe 14\ntom's connInfo") #> ("21", "alice", OK) - ("", "alice_contact", Right (A.REQ aInvId' PQSupportOn _ "tom's connInfo")) <- (alice <#:) - alice #: ("4", "tom", "ACPT " <> aInvId' <> enableKEMStr aPQMode <> " 16\nalice's connInfo") #> ("4", "tom", OK) - ("", "alice", Right (A.CONF tConfId pqSup4 _ "alice's connInfo")) <- (tom <#:) - pqSup4 `shouldBe` tPQ - tom #: ("22", "alice", "LET " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK) - alice <# ("", "tom", A.INFO (CR.connPQEncryption aPQ) "tom's connInfo 2") - alice <# ("", "tom", CON atPQ) - tom <# ("", "alice", CON atPQ) - alice #: ("5", "tom", "SEND F :hi there") #> ("5", "tom", A.MID 4 atPQ) - alice <# ("", "tom", SENT 4) - tom <#= \case ("", "alice", Msg' 4 pq' "hi there") -> pq' == atPQ; _ -> False - tom #: ("23", "alice", "ACK 4") #> ("23", "alice", OK) - -testContactConnRandomIds :: Transport c => (c, InitialKeys) -> (c, PQSupport) -> IO () -testContactConnRandomIds (alice, aPQ) (bob, bPQ) = do - let pq = pqConnectionMode aPQ bPQ - ("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW T CON" <> pqConnModeStr aPQ <> " subscribe") - let cReq' = strEncode cReq - - ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") - ("", aliceContact', Right (A.REQ aInvId PQSupportOn _ "bob's connInfo")) <- (alice <#:) - aliceContact' `shouldBe` aliceContact - - ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aInvId <> enableKEMStr (CR.connPQEncryption aPQ) <> " 16\nalice's connInfo") - ("", aliceConn', Right (A.CONF bConfId pqSup'' _ "alice's connInfo")) <- (bob <#:) - pqSup'' `shouldBe` bPQ - aliceConn' `shouldBe` aliceConn - - bob #: ("12", aliceConn, "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK) - alice <# ("", bobConn, A.INFO (CR.connPQEncryption aPQ) "bob's connInfo 2") - alice <# ("", bobConn, CON pq) - bob <# ("", aliceConn, CON pq) - - alice #: ("3", bobConn, "SEND F :hi") #> ("3", bobConn, A.MID 4 pq) - alice <# ("", bobConn, SENT 4) - bob <#= \case ("", c, Msg' 4 pq' "hi") -> c == aliceConn && pq == pq'; _ -> False - bob #: ("13", aliceConn, "ACK 4") #> ("13", aliceConn, OK) - -testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO () -testRejectContactRequest _ alice bob = do - ("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW T CON subscribe") - let cReq' = strEncode cReq - bob #: ("11", "alice", "JOIN T " <> cReq' <> " subscribe 10\nbob's info") #> ("11", "alice", OK) - ("", "a_contact", Right (A.REQ aInvId PQSupportOn _ "bob's info")) <- (alice <#:) - -- RJCT must use correct contact connection - alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND) - alice #: ("2b", "a_contact", "RJCT " <> aInvId) #> ("2b", "a_contact", OK) - alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") =#> \case ("3", "bob", ERR (A.CMD PROHIBITED _)) -> True; _ -> False - bob #:# "nothing should be delivered to bob" - -testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () -testSubscription _ alice1 alice2 bob = do - (alice1, "alice") `connect` (bob, "bob") - bob #: ("12", "alice", "SEND F 5\nhello") #> ("12", "alice", MID 4) - bob <# ("", "alice", SENT 4) - alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False - alice1 #: ("1", "bob", "ACK 4") #> ("1", "bob", OK) - bob #: ("13", "alice", "SEND F 11\nhello again") #> ("13", "alice", MID 5) - bob <# ("", "alice", SENT 5) - alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False - alice1 #: ("2", "bob", "ACK 5") #> ("2", "bob", OK) - alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK) - alice1 <# ("", "bob", END) - bob #: ("14", "alice", "SEND F 2\nhi") #> ("14", "alice", MID 6) - bob <# ("", "alice", SENT 6) - alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False - alice2 #: ("22", "bob", "ACK 6") #> ("22", "bob", OK) - alice1 #:# "nothing else should be delivered to alice1" - -testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () -testSubscrNotification t (server, _) client = do - client #: ("1", "conn1", "NEW T INV subscribe") =#> \case ("1", "conn1", INV {}) -> True; _ -> False - client #:# "nothing should be delivered to client before the server is killed" - killThread server - client <#. ("", "", DOWN testSMPServer ["conn1"]) - withSmpServer (ATransport t) $ - client <#= \case ("", "conn1", ERR (SMP _ AUTH)) -> True; _ -> False -- this new server does not have the queue - -testMsgDeliveryServerRestart :: forall c. Transport c => (c, InitialKeys) -> (c, PQSupport) -> IO () -testMsgDeliveryServerRestart (alice, aPQ) (bob, bPQ) = do - let pq = pqConnectionMode aPQ bPQ - withServer $ do - connect' (alice, "alice", aPQ) (bob, "bob", bPQ) - bob #: ("1", "alice", "SEND F 2\nhi") #> ("1", "alice", A.MID 4 pq) - bob <# ("", "alice", SENT 4) - alice <#= \case ("", "bob", Msg' _ pq' "hi") -> pq == pq'; _ -> False - alice #: ("11", "bob", "ACK 4") #> ("11", "bob", OK) - alice #:# "nothing else delivered before the server is killed" - - let server = SMPServer "localhost" testPort2 testKeyHash - alice <#. ("", "", DOWN server ["bob"]) - bob #: ("2", "alice", "SEND F 11\nhello again") #> ("2", "alice", A.MID 5 pq) - bob #:# "nothing else delivered before the server is restarted" - alice #:# "nothing else delivered before the server is restarted" - - withServer $ do - bob <# ("", "alice", SENT 5) - alice <#. ("", "", UP server ["bob"]) - alice <#= \case ("", "bob", Msg' _ pq' "hello again") -> pq == pq'; _ -> False - alice #: ("12", "bob", "ACK 5") #> ("12", "bob", OK) - - removeFile testStoreLogFile - where - withServer test' = withSmpServerStoreLogOn (transport @c) testPort2 (const test') `shouldReturn` () - -testServerConnectionAfterError :: forall c. Transport c => TProxy c -> [c] -> IO () -testServerConnectionAfterError t _ = do - withAgent1 $ \bob -> do - withAgent2 $ \alice -> do - withServer $ do - connect (bob, "bob") (alice, "alice") - bob <#. ("", "", DOWN server ["alice"]) - alice <#. ("", "", DOWN server ["bob"]) - alice #: ("1", "bob", "SEND F 5\nhello") #> ("1", "bob", MID 4) - alice #:# "nothing else delivered before the server is restarted" - bob #:# "nothing else delivered before the server is restarted" - - withAgent1 $ \bob -> do - withAgent2 $ \alice -> do - bob #:! ("1", "alice", "SUB") =#> \case ("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT; _ -> False - alice #:! ("1", "bob", "SUB") =#> \case ("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT; _ -> False - withServer $ do - alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False - alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False - bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False - bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False - bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK) - alice #: ("1", "bob", "SEND F 11\nhello again") #> ("1", "bob", MID 5) - alice <# ("", "bob", SENT 5) - bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False - - removeFile testStoreLogFile - removeFile testDB - removeFile testDB2 - where - server = SMPServer "localhost" testPort2 testKeyHash - withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` () - withAgent1 = withAgent agentTestPort testDB 0 - withAgent2 = withAgent agentTestPort2 testDB2 10 - withAgent :: String -> FilePath -> Int -> (c -> IO a) -> IO a - withAgent agentPort agentDB initClientId = withSmpAgentThreadOn_ (ATransport t) (agentPort, testPort2, agentDB) initClientId (pure ()) . const . testSMPAgentClientOn agentPort - -testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO () -testMsgDeliveryAgentRestart t bob = do - let server = SMPServer "localhost" testPort2 testKeyHash - withAgent $ \alice -> do - withServer $ do - connect (bob, "bob") (alice, "alice") - alice #: ("1", "bob", "SEND F 5\nhello") #> ("1", "bob", MID 4) - alice <# ("", "bob", SENT 4) - bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False - bob #: ("11", "alice", "ACK 4") #> ("11", "alice", OK) - bob #:# "nothing else delivered before the server is down" - - bob <#. ("", "", DOWN server ["alice"]) - alice #: ("2", "bob", "SEND F 11\nhello again") #> ("2", "bob", MID 5) - alice #:# "nothing else delivered before the server is restarted" - bob #:# "nothing else delivered before the server is restarted" - - withAgent $ \alice -> do - withServer $ do - tPutRaw alice ("3", "bob", "SUB") - alice <#= \case - (corrId, "bob", cmd) -> - (corrId == "3" && cmd == OK) - || (corrId == "" && cmd == SENT 5) - _ -> False - bob <#=? \case ("", "alice", APC _ (Msg "hello again")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False - bob <#=? \case ("", "alice", APC _ (Msg "hello again")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False - bob #: ("12", "alice", "ACK 5") #> ("12", "alice", OK) - - removeFile testStoreLogFile - removeFile testDB - where - withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` () - withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) 0 (pure ()) . const . testSMPAgentClientOn agentTestPort - -testConcurrentMsgDelivery :: Transport c => TProxy c -> c -> c -> IO () -testConcurrentMsgDelivery _ alice bob = do - connect (alice, "alice") (bob, "bob") - - ("1", "bob2", Right (INV cReq)) <- alice #: ("1", "bob2", "NEW T INV subscribe") - let cReq' = strEncode cReq - bob #: ("11", "alice2", "JOIN T " <> cReq' <> " subscribe 14\nbob's connInfo") #> ("11", "alice2", OK) - ("", "bob2", Right (A.CONF _confId PQSupportOff _ "bob's connInfo")) <- (alice <#:) - -- below commands would be needed to accept bob's connection, but alice does not - -- alice #: ("2", "bob", "LET " <> _confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) - -- bob <# ("", "alice", INFO "alice's connInfo") - -- bob <# ("", "alice", CON) - -- alice <# ("", "bob", CON) - - -- the first connection should not be blocked by the second one - sendMessage (alice, "alice") (bob, "bob") "hello" - -- alice #: ("2", "bob", "SEND F :hello") #> ("2", "bob", MID 1) - -- alice <# ("", "bob", SENT 1) - -- bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False - -- bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK) - bob #: ("14", "alice", "SEND F 9\nhello too") #> ("14", "alice", MID 5) - bob <# ("", "alice", SENT 5) - -- if delivery is blocked it won't go further - alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False - alice #: ("3", "bob", "ACK 5") #> ("3", "bob", OK) - -testMsgDeliveryQuotaExceeded :: Transport c => TProxy c -> c -> c -> IO () -testMsgDeliveryQuotaExceeded _ alice bob = do - connect (alice, "alice") (bob, "bob") - connect (alice, "alice2") (bob, "bob2") - forM_ [1 .. 4 :: Int] $ \i -> do - let corrId = bshow i - msg = "message " <> bshow i - (_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND F :" <> msg) - alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False - (_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND F :over quota") - alice <#= \case ("", "bob", MWARN _ (SMP _ QUOTA)) -> True; _ -> False - - alice #: ("1", "bob2", "SEND F :hello") #> ("1", "bob2", MID 4) - -- if delivery is blocked it won't go further - alice <# ("", "bob2", SENT 4) - -testResumeDeliveryQuotaExceeded :: Transport c => TProxy c -> c -> c -> IO () -testResumeDeliveryQuotaExceeded _ alice bob = do - connect (alice, "alice") (bob, "bob") - forM_ [1 .. 4 :: Int] $ \i -> do - let corrId = bshow i - msg = "message " <> bshow i - (_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND F :" <> msg) - alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False - ("5", "bob", Right (MID 8)) <- alice #: ("5", "bob", "SEND F :over quota") - alice <#= \case ("", "bob", MWARN 8 (SMP _ QUOTA)) -> True; _ -> False - alice #:# "the last message not sent yet" - bob <#= \case ("", "alice", Msg "message 1") -> True; _ -> False - bob #: ("1", "alice", "ACK 4") #> ("1", "alice", OK) - alice #:# "the last message not sent" - bob <#= \case ("", "alice", Msg "message 2") -> True; _ -> False - bob #: ("2", "alice", "ACK 5") #> ("2", "alice", OK) - alice #:# "the last message not sent" - bob <#= \case ("", "alice", Msg "message 3") -> True; _ -> False - bob #: ("3", "alice", "ACK 6") #> ("3", "alice", OK) - alice #:# "the last message not sent" - bob <#= \case ("", "alice", Msg "message 4") -> True; _ -> False - bob #: ("4", "alice", "ACK 7") #> ("4", "alice", OK) - inAnyOrder - (tGetAgent alice) - [ \case ("", c, Right (SENT 8)) -> c == "bob"; _ -> False, - \case ("", c, Right QCONT) -> c == "bob"; _ -> False - ] - bob <#= \case ("", "alice", Msg "over quota") -> True; _ -> False - -- message 8 is skipped because of alice agent sending "QCONT" message - bob #: ("5", "alice", "ACK 9") #> ("5", "alice", OK) - -connect :: Transport c => (c, ByteString) -> (c, ByteString) -> IO () -connect (h1, name1) (h2, name2) = connect' (h1, name1, IKPQOn) (h2, name2, PQSupportOn) - -connect' :: forall c. Transport c => (c, ByteString, InitialKeys) -> (c, ByteString, PQSupport) -> IO () -connect' (h1, name1, pqMode1) (h2, name2, pqMode2) = do - ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW T INV" <> pqConnModeStr pqMode1 <> " subscribe") - let cReq' = strEncode cReq - pq = pqConnectionMode pqMode1 pqMode2 - h2 #: ("c2", name1, "JOIN T " <> cReq' <> enableKEMStr pqMode2 <> " subscribe 5\ninfo2") #> ("c2", name1, OK) - ("", _, Right (A.CONF connId pqSup' _ "info2")) <- (h1 <#:) - pqSup' `shouldBe` CR.connPQEncryption pqMode1 - h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) - h2 <# ("", name1, A.INFO pqMode2 "info1") - h2 <# ("", name1, CON pq) - h1 <# ("", name2, CON pq) - -pqConnectionMode :: InitialKeys -> PQSupport -> PQEncryption -pqConnectionMode pqMode1 pqMode2 = PQEncryption $ supportPQ (CR.connPQEncryption pqMode1) && supportPQ pqMode2 - -enableKEMStr :: PQSupport -> ByteString -enableKEMStr PQSupportOn = " " <> strEncode PQSupportOn -enableKEMStr _ = "" - -pqConnModeStr :: InitialKeys -> ByteString -pqConnModeStr (IKNoPQ PQSupportOff) = "" -pqConnModeStr pq = " " <> strEncode pq - -sendMessage :: Transport c => (c, ConnId) -> (c, ConnId) -> ByteString -> IO () -sendMessage (h1, name1) (h2, name2) msg = do - ("m1", name2', Right (MID mId)) <- h1 #: ("m1", name2, "SEND F :" <> msg) - name2' `shouldBe` name2 - h1 <#= \case ("", n, SENT m) -> n == name2 && m == mId; _ -> False - ("", name1', Right (MSG MsgMeta {recipient = (msgId', _)} _ msg')) <- (h2 <#:) - name1' `shouldBe` name1 - msg' `shouldBe` msg - h2 #: ("m2", name1, "ACK " <> bshow msgId') =#> \case ("m2", n, OK) -> n == name1; _ -> False - --- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) --- connect' h1 h2 = do --- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW T INV subscribe") --- let cReq' = strEncode cReq --- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN T " <> cReq' <> " subscribe 5\ninfo2") --- ("", _, Right (REQ connId _ "info2")) <- (h1 <#:) --- h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False --- h2 <# ("", conn1, INFO "info1") --- h2 <# ("", conn1, CON) --- h1 <# ("", conn2, CON) --- pure (conn1, conn2) - -sampleDhKey :: ByteString -sampleDhKey = "MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" - -syntaxTests :: forall c. Transport c => TProxy c -> Spec -syntaxTests t = do - it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX parseCommand") - describe "NEW" $ do - describe "valid" $ do - it "with correct parameter" $ ("211", "", "NEW T INV subscribe") >#>= \case ("211", _, "INV" : _) -> True; _ -> False - describe "invalid" $ do - it "with incorrect parameter" $ ("222", "", "NEW T hi subscribe") >#> ("222", "", "ERR CMD SYNTAX parseCommand") - - describe "JOIN" $ do - describe "valid" $ do - it "using same server as in invitation" $ - ( "311", - "a", - "JOIN T https://simpex.chat/invitation#/?smp=smp%3A%2F%2F" - <> urlEncode True "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=" - <> "%40localhost%3A5001%2F3456-w%3D%3D%23" - <> urlEncode True sampleDhKey - <> "&v=2" - <> "&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> " subscribe " - <> "14\nbob's connInfo" - ) - >#> ("311", "a", "ERR SMP smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001 AUTH") - describe "invalid" $ do - it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX parseCommand") - where - -- simple test for one command with the expected response - (>#>) :: ARawTransmission -> ARawTransmission -> Expectation - command >#> response = withFrozenCallStack $ smpAgentTest t command `shouldReturn` response - - -- simple test for one command with a predicate for the expected response - (>#>=) :: ARawTransmission -> ((ByteString, ByteString, [ByteString]) -> Bool) -> Expectation - command >#>= p = withFrozenCallStack $ smpAgentTest t command >>= (`shouldSatisfy` p . \(cId, connId, cmd) -> (cId, connId, B.words cmd)) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 25bbdb260..8d1247384 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -86,7 +86,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteS import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Transport (NTFVersion, authBatchCmdsNTFVersion, pattern VersionNTF) @@ -96,7 +96,7 @@ import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion, supportedSMPHandshakes) -import Simplex.Messaging.Util (diffToMicroseconds) +import Simplex.Messaging.Util (bshow, diffToMicroseconds) import Simplex.Messaging.Version (VersionRange (..)) import qualified Simplex.Messaging.Version as V import Simplex.Messaging.Version.Internal (Version (..)) @@ -106,7 +106,7 @@ import UnliftIO import Util import XFTPClient (testXFTPServer) -type AEntityTransmission e = (ACorrId, ConnId, ACommand 'Agent e) +type AEntityTransmission e = (ACorrId, ConnId, AEvent e) -- deriving instance Eq (ValidFileDescription p) @@ -142,49 +142,52 @@ nGet c = withFrozenCallStack $ get' @'AENone c get' :: forall e m. (MonadIO m, AEntityI e, HasCallStack) => AgentClient -> m (AEntityTransmission e) get' c = withFrozenCallStack $ do - (corrId, connId, APC e cmd) <- pGet c + (corrId, connId, AEvt e cmd) <- pGet c case testEquality e (sAEntity @e) of Just Refl -> pure (corrId, connId, cmd) _ -> error $ "unexpected command " <> show cmd -pGet :: forall m. MonadIO m => AgentClient -> m (ATransmission 'Agent) -pGet c = do - t@(_, _, APC _ cmd) <- atomically (readTBQueue $ subQ c) +pGet :: forall m. MonadIO m => AgentClient -> m ATransmission +pGet c = pGet' c True + +pGet' :: forall m. MonadIO m => AgentClient -> Bool -> m ATransmission +pGet' c skipWarn = do + t@(_, _, AEvt _ cmd) <- atomically (readTBQueue $ subQ c) case cmd of CONNECT {} -> pGet c DISCONNECT {} -> pGet c ERR (BROKER _ NETWORK) -> pGet c - MWARN {} -> pGet c + MWARN {} | skipWarn -> pGet c _ -> pure t -pattern CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> ACommand 'Agent e +pattern CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> AEvent e pattern CONF conId srvs connInfo <- A.CONF conId PQSupportOn srvs connInfo -pattern INFO :: ConnInfo -> ACommand 'Agent 'AEConn +pattern INFO :: ConnInfo -> AEvent 'AEConn pattern INFO connInfo = A.INFO PQSupportOn connInfo -pattern REQ :: InvitationId -> NonEmpty SMPServer -> ConnInfo -> ACommand 'Agent e +pattern REQ :: InvitationId -> NonEmpty SMPServer -> ConnInfo -> AEvent e pattern REQ invId srvs connInfo <- A.REQ invId PQSupportOn srvs connInfo -pattern CON :: ACommand 'Agent 'AEConn +pattern CON :: AEvent 'AEConn pattern CON = A.CON PQEncOn -pattern Msg :: MsgBody -> ACommand 'Agent e +pattern Msg :: MsgBody -> AEvent e pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk, pqEncryption = PQEncOn} _ msgBody -pattern Msg' :: AgentMsgId -> PQEncryption -> MsgBody -> ACommand 'Agent e +pattern Msg' :: AgentMsgId -> PQEncryption -> MsgBody -> AEvent e pattern Msg' aMsgId pq msgBody <- MSG MsgMeta {integrity = MsgOk, recipient = (aMsgId, _), pqEncryption = pq} _ msgBody -pattern MsgErr :: AgentMsgId -> MsgErrorType -> MsgBody -> ACommand 'Agent 'AEConn +pattern MsgErr :: AgentMsgId -> MsgErrorType -> MsgBody -> AEvent 'AEConn pattern MsgErr msgId err msgBody <- MSG MsgMeta {recipient = (msgId, _), integrity = MsgError err} _ msgBody -pattern MsgErr' :: AgentMsgId -> MsgErrorType -> PQEncryption -> MsgBody -> ACommand 'Agent 'AEConn +pattern MsgErr' :: AgentMsgId -> MsgErrorType -> PQEncryption -> MsgBody -> AEvent 'AEConn pattern MsgErr' msgId err pq msgBody <- MSG MsgMeta {recipient = (msgId, _), integrity = MsgError err, pqEncryption = pq} _ msgBody -pattern SENT :: AgentMsgId -> ACommand 'Agent 'AEConn +pattern SENT :: AgentMsgId -> AEvent 'AEConn pattern SENT msgId = A.SENT msgId Nothing -pattern Rcvd :: AgentMsgId -> ACommand 'Agent 'AEConn +pattern Rcvd :: AgentMsgId -> AEvent 'AEConn pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMsgId, msgRcptStatus = MROk}] smpCfgVPrev :: ProtocolClientConfig SMPVersion @@ -242,7 +245,7 @@ runRight action = Right x -> pure x Left e -> error $ "Unexpected error: " <> show e -getInAnyOrder :: HasCallStack => AgentClient -> [ATransmission 'Agent -> Bool] -> Expectation +getInAnyOrder :: HasCallStack => AgentClient -> [ATransmission -> Bool] -> Expectation getInAnyOrder c ts = withFrozenCallStack $ inAnyOrder (pGet c) ts inAnyOrder :: (Show a, MonadUnliftIO m, HasCallStack) => m a -> [a -> Bool] -> m () @@ -279,12 +282,20 @@ functionalAPITests t = do withSmpServer t testAgentClient3 it "should establish connection without PQ encryption and enable it" $ withSmpServer t testEnablePQEncryption + describe "Establishing duplex connection, different PQ settings" $ do + testPQMatrix2 t $ runAgentClientTestPQ True describe "Establishing duplex connection v2, different Ratchet versions" $ testRatchetMatrix2 t runAgentClientTest describe "Establish duplex connection via contact address" $ testMatrix2 t runAgentClientContactTest + describe "Establish duplex connection via contact address, different PQ settings" $ do + testPQMatrix2NoInv t $ runAgentClientContactTestPQ True PQSupportOn describe "Establish duplex connection via contact address v2, different Ratchet versions" $ testRatchetMatrix2 t runAgentClientContactTest + describe "Establish duplex connection via contact address, different PQ settings" $ do + testPQMatrix3 t $ runAgentClientContactTestPQ3 True + it "should support rejecting contact request" $ + withSmpServer t testRejectContactRequest describe "Establishing connection asynchronously" $ do it "should connect with initiating client going offline" $ withSmpServer t testAsyncInitiatingOffline @@ -311,6 +322,10 @@ functionalAPITests t = do testDuplicateMessage t it "should report error via msg integrity on skipped messages" $ testSkippedMessages t + it "should connect to the server when server goes up if it initially was down" $ + testDeliveryAfterSubscriptionError t + it "should deliver messages if one of connections has quota exceeded" $ + testMsgDeliveryQuotaExceeded t describe "message expiration" $ do it "should expire one message" $ testExpireMessage t it "should expire multiple messages" $ testExpireManyMessages t @@ -472,7 +487,7 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = let v = basicAuthSMPVersion in allowNew && (isNothing srvAuth || (srvVersion >= v && clntVersion >= v && srvAuth == clntAuth)) -testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False @@ -484,7 +499,7 @@ testMatrix2 t runTest = do it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False -testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testRatchetMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False @@ -495,11 +510,50 @@ testRatchetMatrix2 t runTest = do it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff False it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff False -testServerMatrix2 :: ATransport -> (InitialAgentServers -> IO ()) -> Spec +testServerMatrix2 :: HasCallStack => ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do it "1 server" $ withSmpServer t $ runTest initAgentServers it "2 servers" $ withSmpServer t . withSmpServerOn t testPort2 $ runTest initAgentServers2 +testPQMatrix2 :: HasCallStack => ATransport -> (HasCallStack => (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO ()) -> Spec +testPQMatrix2 = pqMatrix2_ True + +testPQMatrix2NoInv :: HasCallStack => ATransport -> (HasCallStack => (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO ()) -> Spec +testPQMatrix2NoInv = pqMatrix2_ False + +pqMatrix2_ :: HasCallStack => Bool -> ATransport -> (HasCallStack => (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO ()) -> Spec +pqMatrix2_ pqInv t test = do + it "dh/dh handshake" $ runTest $ \a b -> test (a, IKPQOff) (b, PQSupportOff) + it "dh/pq handshake" $ runTest $ \a b -> test (a, IKPQOff) (b, PQSupportOn) + it "pq/dh handshake" $ runTest $ \a b -> test (a, IKPQOn) (b, PQSupportOff) + it "pq/pq handshake" $ runTest $ \a b -> test (a, IKPQOn) (b, PQSupportOn) + when pqInv $ do + it "pq-inv/dh handshake" $ runTest $ \a b -> test (a, IKUsePQ) (b, PQSupportOff) + it "pq-inv/pq handshake" $ runTest $ \a b -> test (a, IKUsePQ) (b, PQSupportOn) + where + runTest = withSmpServerProxy t . runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 + +testPQMatrix3 :: + HasCallStack => + ATransport -> + (HasCallStack => (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> (AgentClient, PQSupport) -> AgentMsgId -> IO ()) -> + Spec +testPQMatrix3 t test = do + it "dh" $ runTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOff) (c, PQSupportOff) + it "dh/dh/pq" $ runTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOff) (c, PQSupportOn) + it "dh/pq/dh" $ runTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOn) (c, PQSupportOff) + it "dh/pq/pq" $ runTest $ \a b c -> test (a, IKPQOff) (b, PQSupportOn) (c, PQSupportOn) + it "pq/dh/dh" $ runTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOff) (c, PQSupportOff) + it "pq/dh/pq" $ runTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOff) (c, PQSupportOn) + it "pq/pq/dh" $ runTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOn) (c, PQSupportOff) + it "pq" $ runTest $ \a b c -> test (a, IKPQOn) (b, PQSupportOn) (c, PQSupportOn) + where + runTest test' = + withSmpServerProxy t $ + runTestCfgServers2 agentProxyCfg agentProxyCfg servers 3 $ \a b baseMsgId -> + withAgent 3 agentProxyCfg servers testDB3 $ \c -> test' a b c baseMsgId + servers = initAgentServersProxy SPMAlways SPFProhibit + runTestCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () runTestCfg2 aCfg bCfg = runTestCfgServers2 aCfg bCfg initAgentServers {-# INLINE runTestCfg2 #-} @@ -509,17 +563,17 @@ runTestCfgServers2 aCfg bCfg servers baseMsgId runTest = withAgentClientsCfgServers2 aCfg bCfg servers $ \a b -> runTest a b baseMsgId {-# INLINE runTestCfgServers2 #-} -withAgentClientsCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO () +withAgentClientsCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> (HasCallStack => AgentClient -> AgentClient -> IO a) -> IO a withAgentClientsCfgServers2 aCfg bCfg servers runTest = withAgent 1 aCfg servers testDB $ \a -> withAgent 2 bCfg servers testDB2 $ \b -> runTest a b -withAgentClientsCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO () +withAgentClientsCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> (HasCallStack => AgentClient -> AgentClient -> IO a) -> IO a withAgentClientsCfg2 aCfg bCfg = withAgentClientsCfgServers2 aCfg bCfg initAgentServers {-# INLINE withAgentClientsCfg2 #-} -withAgentClients2 :: HasCallStack => (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO () +withAgentClients2 :: HasCallStack => (HasCallStack => AgentClient -> AgentClient -> IO a) -> IO a withAgentClients2 = withAgentClientsCfg2 agentCfg agentCfg {-# INLINE withAgentClients2 #-} @@ -530,16 +584,20 @@ withAgentClients3 runTest = runTest a b c runAgentClientTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientTest pqSupport viaProxy alice@AgentClient {} bob baseId = +runAgentClientTest pqSupport viaProxy alice bob baseId = + runAgentClientTestPQ viaProxy (alice, IKNoPQ pqSupport) (bob, pqSupport) baseId + +runAgentClientTestPQ :: HasCallStack => Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () +runAgentClientTestPQ viaProxy (alice, aPQ) (bob, bPQ) baseId = runRight_ $ do - (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" pqSupport SMSubscribe + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing aPQ SMSubscribe + aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" bPQ SMSubscribe ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice - liftIO $ pqSup' `shouldBe` pqSupport + liftIO $ pqSup' `shouldBe` CR.connPQEncryption aPQ allowConnection alice bobId confId "alice's connInfo" - let pqEnc = CR.pqSupportToEnc pqSupport + let pqEnc = PQEncryption $ pqConnectionMode aPQ bPQ get alice ##> ("", bobId, A.CON pqEnc) - get bob ##> ("", aliceId, A.INFO pqSupport "alice's connInfo") + get bob ##> ("", aliceId, A.INFO bPQ "alice's connInfo") get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 let proxySrv = if viaProxy then Just testSMPServer else Nothing @@ -567,6 +625,9 @@ runAgentClientTest pqSupport viaProxy alice@AgentClient {} bob baseId = where msgId = subtract baseId . fst +pqConnectionMode :: InitialKeys -> PQSupport -> Bool +pqConnectionMode pqMode1 pqMode2 = supportPQ (CR.connPQEncryption pqMode1) && supportPQ pqMode2 + testEnablePQEncryption :: HasCallStack => IO () testEnablePQEncryption = withAgentClients2 $ \ca cb -> runRight_ $ do @@ -659,19 +720,23 @@ testAgentClient3 = runAgentClientContactTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () runAgentClientContactTest pqSupport viaProxy alice bob baseId = + runAgentClientContactTestPQ viaProxy pqSupport (alice, IKNoPQ pqSupport) (bob, pqSupport) baseId + +runAgentClientContactTestPQ :: HasCallStack => Bool -> PQSupport -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () +runAgentClientContactTestPQ viaProxy reqPQSupport (alice, aPQ) (bob, bPQ) baseId = runRight_ $ do - (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqSupport) SMSubscribe - aliceId <- A.prepareConnectionToJoin bob 1 True qInfo pqSupport - aliceId' <- A.joinConnection bob 1 (Just aliceId) True qInfo "bob's connInfo" pqSupport SMSubscribe + (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing aPQ SMSubscribe + aliceId <- A.prepareConnectionToJoin bob 1 True qInfo bPQ + aliceId' <- A.joinConnection bob 1 (Just aliceId) True qInfo "bob's connInfo" bPQ SMSubscribe liftIO $ aliceId' `shouldBe` aliceId ("", _, A.REQ invId pqSup' _ "bob's connInfo") <- get alice - liftIO $ pqSup' `shouldBe` pqSupport - bobId <- acceptContact alice True invId "alice's connInfo" PQSupportOn SMSubscribe + liftIO $ pqSup' `shouldBe` reqPQSupport + bobId <- acceptContact alice True invId "alice's connInfo" (CR.connPQEncryption aPQ) SMSubscribe ("", _, A.CONF confId pqSup'' _ "alice's connInfo") <- get bob - liftIO $ pqSup'' `shouldBe` pqSupport + liftIO $ pqSup'' `shouldBe` bPQ allowConnection bob aliceId confId "bob's connInfo" - let pqEnc = CR.pqSupportToEnc pqSupport - get alice ##> ("", bobId, A.INFO pqSupport "bob's connInfo") + let pqEnc = PQEncryption $ pqConnectionMode aPQ bPQ + get alice ##> ("", bobId, A.INFO (CR.connPQEncryption aPQ) "bob's connInfo") get alice ##> ("", bobId, A.CON pqEnc) get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 @@ -700,6 +765,41 @@ runAgentClientContactTest pqSupport viaProxy alice bob baseId = where msgId = subtract baseId . fst +runAgentClientContactTestPQ3 :: HasCallStack => Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () +runAgentClientContactTestPQ3 viaProxy (alice, aPQ) (bob, bPQ) (tom, tPQ) baseId = runRight_ $ do + (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing aPQ SMSubscribe + (bAliceId, bobId, abPQEnc) <- connectViaContact bob bPQ qInfo + sentMessages abPQEnc alice bobId bob bAliceId + (tAliceId, tomId, atPQEnc) <- connectViaContact tom tPQ qInfo + sentMessages atPQEnc alice tomId tom tAliceId + where + msgId = subtract baseId . fst + connectViaContact b pq qInfo = do + aId <- A.prepareConnectionToJoin b 1 True qInfo pq + aId' <- A.joinConnection b 1 (Just aId) True qInfo "bob's connInfo" pq SMSubscribe + liftIO $ aId' `shouldBe` aId + ("", _, A.REQ invId pqSup' _ "bob's connInfo") <- get alice + liftIO $ pqSup' `shouldBe` PQSupportOn + bId <- acceptContact alice True invId "alice's connInfo" (CR.connPQEncryption aPQ) SMSubscribe + ("", _, A.CONF confId pqSup'' _ "alice's connInfo") <- get b + liftIO $ pqSup'' `shouldBe` pq + allowConnection b aId confId "bob's connInfo" + let pqEnc = PQEncryption $ pqConnectionMode aPQ pq + get alice ##> ("", bId, A.INFO (CR.connPQEncryption aPQ) "bob's connInfo") + get alice ##> ("", bId, A.CON pqEnc) + get b ##> ("", aId, A.CON pqEnc) + pure (aId, bId, pqEnc) + sentMessages pqEnc a bId b aId = do + let proxySrv = if viaProxy then Just testSMPServer else Nothing + 1 <- msgId <$> A.sendMessage a bId pqEnc SMP.noMsgFlags "hello" + get a ##> ("", bId, A.SENT (baseId + 1) proxySrv) + get b =##> \case ("", c, Msg' _ pq "hello") -> c == aId && pq == pqEnc; _ -> False + ackMessage b aId (baseId + 1) Nothing + 2 <- msgId <$> A.sendMessage b aId pqEnc SMP.noMsgFlags "hello too" + get b ##> ("", aId, A.SENT (baseId + 2) proxySrv) + get a =##> \case ("", c, Msg' _ pq "hello too") -> c == bId && pq == pqEnc; _ -> False + ackMessage a bId (baseId + 2) Nothing + noMessages :: HasCallStack => AgentClient -> String -> Expectation noMessages c err = tryGet `shouldReturn` () where @@ -708,6 +808,18 @@ noMessages c err = tryGet `shouldReturn` () Just msg -> error $ err <> ": " <> show msg _ -> return () +testRejectContactRequest :: HasCallStack => IO () +testRejectContactRequest = + withAgentClients2 $ \alice bob -> runRight_ $ do + (addrConnId, qInfo) <- A.createConnection alice 1 True SCMContact Nothing IKPQOn SMSubscribe + aliceId <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn + aliceId' <- A.joinConnection bob 1 (Just aliceId) True qInfo "bob's connInfo" PQSupportOn SMSubscribe + liftIO $ aliceId' `shouldBe` aliceId + ("", _, A.REQ invId PQSupportOn _ "bob's connInfo") <- get alice + liftIO $ runExceptT (rejectContact alice "abcd" invId) `shouldReturn` Left (CONN NOT_FOUND) + rejectContact alice addrConnId invId + liftIO $ noMessages bob "nothing delivered to bob" + testAsyncInitiatingOffline :: HasCallStack => IO () testAsyncInitiatingOffline = withAgent 2 agentCfg initAgentServers testDB2 $ \bob -> runRight_ $ do @@ -1072,6 +1184,53 @@ testSkippedMessages t = do disposeAgentClient alice2 disposeAgentClient bob2 +testDeliveryAfterSubscriptionError :: HasCallStack => ATransport -> IO () +testDeliveryAfterSubscriptionError t = do + (aId, bId) <- withAgentClients2 $ \a b -> do + (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ makeConnection a b + nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False + nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False + 4 <- runRight $ sendMessage a bId SMP.noMsgFlags "hello" + liftIO $ noMessages b "not delivered" + pure (aId, bId) + + withAgentClients2 $ \a b -> do + Left (BROKER _ NETWORK) <- runExceptT $ subscribeConnection a bId + Left (BROKER _ NETWORK) <- runExceptT $ subscribeConnection b aId + pure () + withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do + withUP a bId $ \case ("", c, SENT 4) -> c == bId; _ -> False + withUP b aId $ \case ("", c, Msg "hello") -> c == aId; _ -> False + ackMessage b aId 4 Nothing + +testMsgDeliveryQuotaExceeded :: HasCallStack => ATransport -> IO () +testMsgDeliveryQuotaExceeded t = + withAgentClients2 $ \a b -> withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do + (aId, bId) <- makeConnection a b + (aId', bId') <- makeConnection a b + forM_ ([1 .. 4] :: [Int]) $ \i -> do + mId <- sendMessage a bId SMP.noMsgFlags $ "message " <> bshow i + get a =##> \case ("", c, SENT mId') -> bId == c && mId == mId'; _ -> False + 8 <- sendMessage a bId SMP.noMsgFlags "over quota" + pGet' a False =##> \case ("", c, AEvt _ (MWARN 8 (SMP _ QUOTA))) -> bId == c; _ -> False + 4 <- sendMessage a bId' SMP.noMsgFlags "hello" + get a =##> \case ("", c, SENT 4) -> bId' == c; _ -> False + get b =##> \case ("", c, Msg "message 1") -> aId == c; _ -> False + get b =##> \case ("", c, Msg "hello") -> aId' == c; _ -> False + ackMessage b aId' 4 Nothing + ackMessage b aId 4 Nothing + get b =##> \case ("", c, Msg "message 2") -> aId == c; _ -> False + ackMessage b aId 5 Nothing + get b =##> \case ("", c, Msg "message 3") -> aId == c; _ -> False + ackMessage b aId 6 Nothing + get b =##> \case ("", c, Msg "message 4") -> aId == c; _ -> False + ackMessage b aId 7 Nothing + get a =##> \case ("", c, QCONT) -> bId == c; _ -> False + get b =##> \case ("", c, Msg "over quota") -> aId == c; _ -> False + ackMessage b aId 9 Nothing -- msg 8 was QCONT + get a =##> \case ("", c, SENT 8) -> bId == c; _ -> False + liftIO $ concurrently_ (noMessages a "no more events") (noMessages b "no more events") + testExpireMessage :: HasCallStack => ATransport -> IO () testExpireMessage t = withAgent 1 agentCfg {messageTimeout = 1, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> @@ -1124,8 +1283,8 @@ withUP a bId p = liftIO $ getInAnyOrder a - [ \case ("", "", APC SAENone (UP _ [c])) -> c == bId; _ -> False, - \case (corrId, c, APC SAEConn cmd) -> c == bId && p (corrId, c, cmd); _ -> False + [ \case ("", "", AEvt SAENone (UP _ [c])) -> c == bId; _ -> False, + \case (corrId, c, AEvt SAEConn cmd) -> c == bId && p (corrId, c, cmd); _ -> False ] testExpireMessageQuota :: HasCallStack => ATransport -> IO () @@ -1147,8 +1306,8 @@ testExpireMessageQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testP get b' =##> \case ("", c, Msg "1") -> c == aId; _ -> False ackMessage b' aId 4 Nothing liftIO . getInAnyOrder a $ - [ \case ("", c, APC SAEConn (SENT 6)) -> c == bId; _ -> False, - \case ("", c, APC SAEConn QCONT) -> c == bId; _ -> False + [ \case ("", c, AEvt SAEConn (SENT 6)) -> c == bId; _ -> False, + \case ("", c, AEvt SAEConn QCONT) -> c == bId; _ -> False ] get b' =##> \case ("", c, MsgErr 6 (MsgSkipped 4 4) "3") -> c == aId; _ -> False ackMessage b' aId 6 Nothing @@ -1184,8 +1343,8 @@ testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} get b' =##> \case ("", c, Msg "1") -> c == aId; _ -> False ackMessage b' aId 4 Nothing liftIO . getInAnyOrder a $ - [ \case ("", c, APC SAEConn (SENT 8)) -> c == bId; _ -> False, - \case ("", c, APC SAEConn QCONT) -> c == bId; _ -> False + [ \case ("", c, AEvt SAEConn (SENT 8)) -> c == bId; _ -> False, + \case ("", c, AEvt SAEConn QCONT) -> c == bId; _ -> False ] get b' =##> \case ("", c, MsgErr 6 (MsgSkipped 4 6) "5") -> c == aId; _ -> False ackMessage b' aId 6 Nothing @@ -1258,9 +1417,9 @@ ratchetSyncP cId rss = \case cId' == cId && rss' == rss && ratchetSyncState == rss _ -> False -ratchetSyncP' :: ConnId -> RatchetSyncState -> ATransmission 'Agent -> Bool +ratchetSyncP' :: ConnId -> RatchetSyncState -> ATransmission -> Bool ratchetSyncP' cId rss = \case - (_, cId', APC SAEConn (RSYNC rss' _ ConnectionStats {ratchetSyncState})) -> + (_, cId', AEvt SAEConn (RSYNC rss' _ ConnectionStats {ratchetSyncState})) -> cId' == cId && rss' == rss && ratchetSyncState == rss _ -> False @@ -1285,9 +1444,9 @@ testRatchetSyncServerOffline t = withAgentClients2 $ \alice bob -> do exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 disposeAgentClient bob2 -serverUpP :: ATransmission 'Agent -> Bool +serverUpP :: ATransmission -> Bool serverUpP = \case - ("", "", APC SAENone (UP _ _)) -> True + ("", "", AEvt SAENone (UP _ _)) -> True _ -> False testRatchetSyncClientRestart :: HasCallStack => ATransport -> IO () @@ -1436,8 +1595,8 @@ testInactiveNoSubs t = do withSmpServerConfigOn t cfg' testPort $ \_ -> withAgent 1 agentCfg initAgentServers testDB $ \alice -> do runRight_ . void $ createConnection alice 1 True SCMInvitation Nothing SMOnlyCreate -- do not subscribe to pass noSubscriptions check - Just (_, _, APC SAENone (CONNECT _ _)) <- timeout 2000000 $ atomically (readTBQueue $ subQ alice) - Just (_, _, APC SAENone (DISCONNECT _ _)) <- timeout 5000000 $ atomically (readTBQueue $ subQ alice) + Just (_, _, AEvt SAENone (CONNECT _ _)) <- timeout 2000000 $ atomically (readTBQueue $ subQ alice) + Just (_, _, AEvt SAENone (DISCONNECT _ _)) <- timeout 5000000 $ atomically (readTBQueue $ subQ alice) pure () testInactiveWithSubs :: ATransport -> IO () @@ -1513,13 +1672,13 @@ testSuspendingAgentCompleteSending t = withAgentClients2 $ \a b -> do liftIO $ suspendAgent b 5000000 withSmpServerStoreLogOn t testPort $ \_ -> runRight_ @AgentErrorType $ do - pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC SAEConn (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, AEvt SAEConn (SENT 5)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, AEvt SAEConn (SENT 5)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, AEvt SAEConn (SENT 6)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False ("", "", SUSPENDED) <- nGet b - pGet a =##> \case ("", c, APC _ (Msg "hello too")) -> c == bId; ("", "", APC _ UP {}) -> True; _ -> False - pGet a =##> \case ("", c, APC _ (Msg "hello too")) -> c == bId; ("", "", APC _ UP {}) -> True; _ -> False + pGet a =##> \case ("", c, AEvt _ (Msg "hello too")) -> c == bId; ("", "", AEvt _ UP {}) -> True; _ -> False + pGet a =##> \case ("", c, AEvt _ (Msg "hello too")) -> c == bId; ("", "", AEvt _ UP {}) -> True; _ -> False ackMessage a bId 5 Nothing get a =##> \case ("", c, Msg "how are you?") -> c == bId; _ -> False ackMessage a bId 6 Nothing @@ -1810,8 +1969,8 @@ testWaitDelivery t = liftIO $ getInAnyOrder bob - [ \case ("", "", APC SAENone (UP _ [cId])) -> cId == aliceId; _ -> False, - \case ("", cId, APC SAEConn (Msg "how are you?")) -> cId == aliceId; _ -> False + [ \case ("", "", AEvt SAENone (UP _ [cId])) -> cId == aliceId; _ -> False, + \case ("", cId, AEvt SAEConn (Msg "how are you?")) -> cId == aliceId; _ -> False ] ackMessage bob aliceId (baseId + 3) Nothing get bob =##> \case ("", c, Msg "message 1") -> c == aliceId; _ -> False @@ -1947,8 +2106,8 @@ testWaitDeliveryTimeout2 t = liftIO $ getInAnyOrder bob - [ \case ("", "", APC SAENone (UP _ [cId])) -> cId == aliceId; _ -> False, - \case ("", cId, APC SAEConn (Msg "how are you?")) -> cId == aliceId; _ -> False + [ \case ("", "", AEvt SAENone (UP _ [cId])) -> cId == aliceId; _ -> False, + \case ("", cId, AEvt SAEConn (Msg "how are you?")) -> cId == aliceId; _ -> False ] liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" @@ -1974,10 +2133,10 @@ testJoinConnectionAsyncReplyError t = do get b =##> \case ("2", c, OK) -> c == aId; _ -> False confId <- withSmpServerStoreLogOn t testPort $ \_ -> do pGet a >>= \case - ("", "", APC _ (UP _ [_])) -> do + ("", "", AEvt _ (UP _ [_])) -> do ("", _, CONF confId _ "bob's connInfo") <- get a pure confId - ("", _, APC _ (CONF confId _ "bob's connInfo")) -> do + ("", _, AEvt _ (CONF confId _ "bob's connInfo")) -> do ("", "", UP _ [_]) <- nGet a pure confId r -> error $ "unexpected response " <> show r @@ -1988,8 +2147,8 @@ testJoinConnectionAsyncReplyError t = do ConnectionStats {rcvQueuesInfo = [RcvQueueInfo {}], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId pure () withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do - pGet a =##> \case ("3", c, APC _ OK) -> c == bId; ("", "", APC _ (UP _ [c])) -> c == bId; _ -> False - pGet a =##> \case ("3", c, APC _ OK) -> c == bId; ("", "", APC _ (UP _ [c])) -> c == bId; _ -> False + pGet a =##> \case ("3", c, AEvt _ OK) -> c == bId; ("", "", AEvt _ (UP _ [c])) -> c == bId; _ -> False + pGet a =##> \case ("3", c, AEvt _ OK) -> c == bId; ("", "", AEvt _ (UP _ [c])) -> c == bId; _ -> False get a ##> ("", bId, CON) get b ##> ("", aId, INFO "alice's connInfo") get b ##> ("", aId, CON) @@ -2261,20 +2420,20 @@ testAbortSwitchStartedReinitiate servers = do withB :: (AgentClient -> IO a) -> IO a withB = withAgent 2 agentCfg servers testDB2 -switchPhaseRcvP :: ConnId -> SwitchPhase -> [Maybe RcvSwitchStatus] -> ATransmission 'Agent -> Bool +switchPhaseRcvP :: ConnId -> SwitchPhase -> [Maybe RcvSwitchStatus] -> ATransmission -> Bool switchPhaseRcvP cId sphase swchStatuses = switchPhaseP cId QDRcv sphase (\stats -> rcvSwchStatuses' stats == swchStatuses) -switchPhaseSndP :: ConnId -> SwitchPhase -> [Maybe SndSwitchStatus] -> ATransmission 'Agent -> Bool +switchPhaseSndP :: ConnId -> SwitchPhase -> [Maybe SndSwitchStatus] -> ATransmission -> Bool switchPhaseSndP cId sphase swchStatuses = switchPhaseP cId QDSnd sphase (\stats -> sndSwchStatuses' stats == swchStatuses) -switchPhaseP :: ConnId -> QueueDirection -> SwitchPhase -> (ConnectionStats -> Bool) -> ATransmission 'Agent -> Bool +switchPhaseP :: ConnId -> QueueDirection -> SwitchPhase -> (ConnectionStats -> Bool) -> ATransmission -> Bool switchPhaseP cId qd sphase statsP = \case - (_, cId', APC SAEConn (SWITCH qd' sphase' stats)) -> cId' == cId && qd' == qd && sphase' == sphase && statsP stats + (_, cId', AEvt SAEConn (SWITCH qd' sphase' stats)) -> cId' == cId && qd' == qd && sphase' == sphase && statsP stats _ -> False -errQueueNotFoundP :: ConnId -> ATransmission 'Agent -> Bool +errQueueNotFoundP :: ConnId -> ATransmission -> Bool errQueueNotFoundP cId = \case - (_, cId', APC SAEConn (ERR AGENT {agentErr = A_QUEUE {queueErr = "QKEY: queue address not found in connection"}})) -> cId' == cId + (_, cId', AEvt SAEConn (ERR AGENT {agentErr = A_QUEUE {queueErr = "QKEY: queue address not found in connection"}})) -> cId' == cId _ -> False testCannotAbortSwitchSecured :: HasCallStack => InitialAgentServers -> IO () diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index a7c3fb25a..01eab9555 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -56,7 +56,7 @@ import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMes import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore') import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers) import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT) -import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken) +import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore, getSavedNtfToken, reopenSQLiteStore) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol @@ -161,11 +161,12 @@ testNtfMatrix t runTest = do it "servers: next SMP v7, curr NTF v2; clients: curr/new" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfgV7 runTest runNtfTestCfg :: ATransport -> ServerConfig -> NtfServerConfig -> AgentConfig -> AgentConfig -> (APNSMockServer -> AgentClient -> AgentClient -> IO ()) -> IO () -runNtfTestCfg t smpCfg ntfCfg aCfg bCfg runTest = +runNtfTestCfg t smpCfg ntfCfg aCfg bCfg runTest = do withSmpServerConfigOn t smpCfg testPort $ \_ -> withAPNSMockServer $ \apns -> withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t)]} $ \_ -> withAgentClientsCfg2 aCfg bCfg $ runTest apns + threadDelay 100000 testNotificationToken :: APNSMockServer -> IO () testNotificationToken APNSMockServer {apnsQ} = do @@ -345,7 +346,7 @@ testRunNTFServerTests t srv = testProtocolServer a 1 $ ProtoServerWithAuth srv Nothing testNotificationSubscriptionExistingConnection :: APNSMockServer -> AgentClient -> AgentClient -> IO () -testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@AgentClient {agentEnv = Env {config = aliceCfg}} bob = do +testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@AgentClient {agentEnv = Env {config = aliceCfg, store}} bob = do (bobId, aliceId, nonce, message) <- runRight $ do -- establish connection (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe @@ -376,11 +377,21 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@Agen -- alice client already has subscription for the connection Left (CMD PROHIBITED _) <- runExceptT $ getNotificationMessage alice nonce message + threadDelay 200000 + suspendAgent alice 0 + closeSQLiteStore store + threadDelay 200000 + -- aliceNtf client doesn't have subscription and is allowed to get notification message withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do (_, [SMPMsgMeta {msgFlags = MsgFlags True}]) <- getNotificationMessage aliceNtf nonce message pure () + threadDelay 200000 + reopenSQLiteStore store + foregroundAgent alice + threadDelay 200000 + runRight_ $ do get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False ackMessage alice bobId (baseId + 1) Nothing diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 63466b9d7..039e26090 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -663,7 +663,7 @@ testGetPendingServerCommand st = do Right (Just PendingCommand {corrId = corrId'}) <- getPendingServerCommand db (Just smpServer1) corrId' `shouldBe` "4" where - command = AClientCommand $ APC SAEConn $ NEW True (ACM SCMInvitation) (IKNoPQ PQSupportOn) SMSubscribe + command = AClientCommand $ NEW True (ACM SCMInvitation) (IKNoPQ PQSupportOn) SMSubscribe corruptCmd :: DB.Connection -> ByteString -> ConnId -> IO () corruptCmd db corrId connId = DB.execute db "UPDATE commands SET command = cast('bad' as blob) WHERE conn_id = ? AND corr_id = ?" (connId, corrId) diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs deleted file mode 100644 index 4466c4933..000000000 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ /dev/null @@ -1,111 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -Wno-orphans #-} - -module CoreTests.ProtocolErrorTests where - -import GHC.Generics (Generic) -import Generic.Random (genericArbitraryU) -import Simplex.FileTransfer.Transport (XFTPErrorType (..)) -import Simplex.Messaging.Agent.Protocol -import qualified Simplex.Messaging.Agent.Protocol as Agent -import Simplex.Messaging.Client (ProxyClientError (..)) -import Simplex.Messaging.Encoding -import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) -import qualified Simplex.Messaging.Protocol as SMP -import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) -import Simplex.RemoteControl.Types (RCErrorType (..)) -import Test.Hspec -import Test.Hspec.QuickCheck (modifyMaxSuccess) -import Test.QuickCheck - -protocolErrorTests :: Spec -protocolErrorTests = modifyMaxSuccess (const 1000) $ do - describe "errors parsing / serializing" $ do - it "should parse SMP protocol errors" . property . forAll possibleErrorType $ \err -> - smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property . forAll possibleAgentErrorType $ \err -> - strDecode (strEncode err) == Right err - where - possibleErrorType :: Gen ErrorType - possibleErrorType = arbitrary >>= \e -> if skipErrorType e then discard else pure e - possibleAgentErrorType :: Gen AgentErrorType - possibleAgentErrorType = - arbitrary >>= \case - BROKER srv _ | hasSpaces srv -> discard - SMP srv e | hasSpaces srv || skipErrorType e -> discard - NTF srv e | hasSpaces srv || skipErrorType e -> discard - XFTP srv _ | hasSpaces srv -> discard - Agent.PROXY pxy srv _ | hasSpaces pxy || hasSpaces srv -> discard - Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard - Agent.PROXY _ _ (ProxyUnexpectedResponse e) | hasUnicode e -> discard - Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard - ok -> pure ok - hasSpaces :: String -> Bool - hasSpaces = any (== ' ') - hasUnicode :: String -> Bool - hasUnicode = any (>= '\255') - skipErrorType = \case - SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e - SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> hasUnicode s - SMP.PROXY (SMP.BROKER (RESPONSE s)) -> hasUnicode s - _ -> False - -deriving instance Generic AgentErrorType - -deriving instance Generic CommandErrorType - -deriving instance Generic ConnectionErrorType - -deriving instance Generic ProxyClientError - -deriving instance Generic BrokerErrorType - -deriving instance Generic SMPAgentError - -deriving instance Generic AgentCryptoError - -deriving instance Generic ErrorType - -deriving instance Generic CommandError - -deriving instance Generic SMP.ProxyError - -deriving instance Generic TransportError - -deriving instance Generic HandshakeError - -deriving instance Generic XFTPErrorType - -deriving instance Generic RCErrorType - -instance Arbitrary AgentErrorType where arbitrary = genericArbitraryU - -instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU - -instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU - -instance Arbitrary ProxyClientError where arbitrary = genericArbitraryU - -instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU - -instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU - -instance Arbitrary AgentCryptoError where arbitrary = genericArbitraryU - -instance Arbitrary ErrorType where arbitrary = genericArbitraryU - -instance Arbitrary CommandError where arbitrary = genericArbitraryU - -instance Arbitrary SMP.ProxyError where arbitrary = genericArbitraryU - -instance Arbitrary TransportError where arbitrary = genericArbitraryU - -instance Arbitrary HandshakeError where arbitrary = genericArbitraryU - -instance Arbitrary XFTPErrorType where arbitrary = genericArbitraryU - -instance Arbitrary RCErrorType where arbitrary = genericArbitraryU diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index aee3c8cb0..c5de1533b 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -10,54 +10,20 @@ module SMPAgentClient where -import Control.Monad -import Control.Monad.IO.Unlift -import qualified Data.ByteString.Char8 as B import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import qualified Database.SQLite.Simple as SQL -import Network.Socket (ServiceName) import NtfClient (ntfTestPort) -import SMPClient - ( proxyVRange, - serverBracket, - testKeyHash, - testPort, - testPort2, - withSmpServer, - withSmpServerOn, - withSmpServerThreadOn, - ) +import SMPClient (proxyVRange, testPort) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval -import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) -import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, defaultNetworkConfig, defaultSMPClientConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) -import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) import Simplex.Messaging.Transport -import Simplex.Messaging.Transport.Client -import Test.Hspec -import UnliftIO.Concurrent -import UnliftIO.Directory import XFTPClient (testXFTPServer) -agentTestHost :: NonEmpty TransportHost -agentTestHost = "localhost" - -agentTestPort :: ServiceName -agentTestPort = "5010" - -agentTestPort2 :: ServiceName -agentTestPort2 = "5011" - -agentTestPort3 :: ServiceName -agentTestPort3 = "5012" - testDB :: FilePath testDB = "tests/tmp/smp-agent.test.protocol.db" @@ -67,114 +33,6 @@ testDB2 = "tests/tmp/smp-agent2.test.protocol.db" testDB3 :: FilePath testDB3 = "tests/tmp/smp-agent3.test.protocol.db" -smpAgentTest :: forall c. Transport c => TProxy c -> ARawTransmission -> IO ARawTransmission -smpAgentTest _ cmd = runSmpAgentTest $ \(h :: c) -> tPutRaw h cmd >> get h - where - get h = do - t@(_, _, cmdStr) <- tGetRaw h - case parseAll networkCommandP cmdStr of - Right (ACmd SAgent _ CONNECT {}) -> get h - Right (ACmd SAgent _ DISCONNECT {}) -> get h - _ -> pure t - -runSmpAgentTest :: forall c a. Transport c => (c -> IO a) -> IO a -runSmpAgentTest test = withSmpServer t . withSmpAgent t $ testSMPAgentClient test - where - t = transport @c - -runSmpAgentServerTest :: forall c a. Transport c => ((ThreadId, ThreadId) -> c -> IO a) -> IO a -runSmpAgentServerTest test = - withSmpServerThreadOn t testPort $ - \server -> withSmpAgentThreadOn t (agentTestPort, testPort, testDB) $ - \agent -> testSMPAgentClient $ test (server, agent) - where - t = transport @c - -smpAgentServerTest :: Transport c => ((ThreadId, ThreadId) -> c -> IO ()) -> Expectation -smpAgentServerTest test' = runSmpAgentServerTest test' `shouldReturn` () - -runSmpAgentTestN :: forall c a. Transport c => [(ServiceName, ServiceName, FilePath)] -> ([c] -> IO a) -> IO a -runSmpAgentTestN agents test = withSmpServer t $ run agents [] - where - run :: [(ServiceName, ServiceName, FilePath)] -> [c] -> IO a - run [] hs = test hs - run (a@(p, _, _) : as) hs = withSmpAgentOn t a $ testSMPAgentClientOn p $ \h -> run as (h : hs) - t = transport @c - -runSmpAgentTestN_1 :: forall c a. Transport c => Int -> ([c] -> IO a) -> IO a -runSmpAgentTestN_1 nClients test = withSmpServer t . withSmpAgent t $ run nClients [] - where - run :: Int -> [c] -> IO a - run 0 hs = test hs - run n hs = testSMPAgentClient $ \h -> run (n - 1) (h : hs) - t = transport @c - -smpAgentTestN :: Transport c => [(ServiceName, ServiceName, FilePath)] -> ([c] -> IO ()) -> Expectation -smpAgentTestN agents test' = runSmpAgentTestN agents test' `shouldReturn` () - -smpAgentTestN_1 :: Transport c => Int -> ([c] -> IO ()) -> Expectation -smpAgentTestN_1 n test' = runSmpAgentTestN_1 n test' `shouldReturn` () - -smpAgentTest2_2_2 :: forall c. Transport c => (c -> c -> IO ()) -> Expectation -smpAgentTest2_2_2 test' = - withSmpServerOn (transport @c) testPort2 $ - smpAgentTest2_2_2_needs_server test' - -smpAgentTest2_2_2_needs_server :: forall c. Transport c => (c -> c -> IO ()) -> Expectation -smpAgentTest2_2_2_needs_server test' = - smpAgentTestN - [ (agentTestPort, testPort, testDB), - (agentTestPort2, testPort2, testDB2) - ] - _test - where - _test [h1, h2] = test' h1 h2 - _test _ = error "expected 2 handles" - -smpAgentTest2_2_1 :: Transport c => (c -> c -> IO ()) -> Expectation -smpAgentTest2_2_1 test' = - smpAgentTestN - [ (agentTestPort, testPort, testDB), - (agentTestPort2, testPort, testDB2) - ] - _test - where - _test [h1, h2] = test' h1 h2 - _test _ = error "expected 2 handles" - -smpAgentTest2_1_1 :: Transport c => (c -> c -> IO ()) -> Expectation -smpAgentTest2_1_1 test' = smpAgentTestN_1 2 _test - where - _test [h1, h2] = test' h1 h2 - _test _ = error "expected 2 handles" - -smpAgentTest3 :: Transport c => (c -> c -> c -> IO ()) -> Expectation -smpAgentTest3 test' = - smpAgentTestN - [ (agentTestPort, testPort, testDB), - (agentTestPort2, testPort, testDB2), - (agentTestPort3, testPort, testDB3) - ] - _test - where - _test [h1, h2, h3] = test' h1 h2 h3 - _test _ = error "expected 3 handles" - -smpAgentTest3_1_1 :: Transport c => (c -> c -> c -> IO ()) -> Expectation -smpAgentTest3_1_1 test' = smpAgentTestN_1 3 _test - where - _test [h1, h2, h3] = test' h1 h2 h3 - _test _ = error "expected 3 handles" - -smpAgentTest1_1_1 :: forall c. Transport c => (c -> IO ()) -> Expectation -smpAgentTest1_1_1 test' = - smpAgentTestN - [(agentTestPort2, testPort2, testDB2)] - _test - where - _test [h] = test' h - _test _ = error "expected 1 handle" - testSMPServer :: SMPServer testSMPServer = "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001" @@ -206,7 +64,7 @@ initAgentServersProxy smpProxyMode smpProxyFallback = agentCfg :: AgentConfig agentCfg = defaultAgentConfig - { tcpPort = Just agentTestPort, + { tcpPort = Nothing, tbqSize = 4, -- database = testDB, smpCfg = defaultSMPClientConfig {qSize = 1, defaultTransport = (testPort, transport @TLS), networkConfig}, @@ -232,39 +90,5 @@ fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} fastMessageRetryInterval :: RetryInterval2 fastMessageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} -withSmpAgentThreadOn_ :: ATransport -> (ServiceName, ServiceName, FilePath) -> Int -> IO () -> (ThreadId -> IO a) -> IO a -withSmpAgentThreadOn_ t (port', smpPort', db') initClientId afterProcess = - let cfg' = agentCfg {tcpPort = Just port'} - initServers' = initAgentServers {smp = userServers [ProtoServerWithAuth (SMPServer "localhost" smpPort' testKeyHash) Nothing]} - in serverBracket - ( \started -> do - Right st <- liftIO $ createAgentStore db' "" False MCError - when (dbNew st) . liftIO $ withTransaction' st (`SQL.execute_` "INSERT INTO users (user_id) VALUES (1)") - runSMPAgentBlocking t cfg' initServers' st initClientId started - ) - afterProcess - userServers :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ProtoServerWithAuth p)) userServers srvs = M.fromList [(1, srvs)] - -withSmpAgentThreadOn :: ATransport -> (ServiceName, ServiceName, FilePath) -> (ThreadId -> IO a) -> IO a -withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a 0 $ removeFile db' - -withSmpAgentOn :: ATransport -> (ServiceName, ServiceName, FilePath) -> IO a -> IO a -withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const - -withSmpAgent :: ATransport -> IO a -> IO a -withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB) - -testSMPAgentClientOn :: Transport c => ServiceName -> (c -> IO a) -> IO a -testSMPAgentClientOn port' client = do - Right useHost <- pure $ chooseTransportHost defaultNetworkConfig agentTestHost - runTransportClient defaultTransportClientConfig Nothing useHost port' (Just testKeyHash) $ \h -> do - line <- getLn h - if line == "Welcome to SMP agent v" <> B.pack simplexMQVersion - then client h - else do - error $ "wrong welcome message: " <> B.unpack line - -testSMPAgentClient :: Transport c => (c -> IO a) -> IO a -testSMPAgentClient = testSMPAgentClientOn agentTestPort diff --git a/tests/Test.hs b/tests/Test.hs index f9fb2a2c0..98d902163 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -11,7 +11,6 @@ import CoreTests.BatchingTests import CoreTests.CryptoFileTests import CoreTests.CryptoTests import CoreTests.EncodingTests -import CoreTests.ProtocolErrorTests import CoreTests.RetryIntervalTests import CoreTests.TRcvQueuesTests import CoreTests.UtilTests @@ -49,7 +48,6 @@ main = do describe "Core tests" $ do describe "Batching tests" batchingTests describe "Encoding tests" encodingTests - describe "Protocol error tests" protocolErrorTests describe "Version range" versionRangeTests describe "Encryption tests" cryptoTests describe "Encrypted files tests" cryptoFileTests diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index e4cf3d704..4580652e2 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -28,7 +28,7 @@ import Simplex.FileTransfer.Transport (XFTPErrorType (AUTH)) import Simplex.Messaging.Agent (AgentClient, testProtocolServer, xftpDeleteRcvFile, xftpDeleteSndFileInternal, xftpDeleteSndFileRemote, xftpReceiveFile, xftpSendDescription, xftpSendFile, xftpStartWorkers) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, xftpCfg) -import Simplex.Messaging.Agent.Protocol (ACommand (..), AgentErrorType (..), BrokerErrorType (..), RcvFileId, SndFileId, noAuthSrv) +import Simplex.Messaging.Agent.Protocol (AEvent (..), AgentErrorType (..), BrokerErrorType (..), RcvFileId, SndFileId, noAuthSrv) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs) import qualified Simplex.Messaging.Crypto.File as CF From 3c0cd7efcc3d3058d940c7a9667faef2dc6de6cc Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:44:32 +0400 Subject: [PATCH 091/125] agent: separate type for agent file errors (#1185) --- src/Simplex/FileTransfer/Agent.hs | 70 +++++++++++++---------- src/Simplex/FileTransfer/Transport.hs | 10 ---- src/Simplex/FileTransfer/Types.hs | 47 ++++++++++++++- src/Simplex/Messaging/Agent.hs | 1 + src/Simplex/Messaging/Agent/Env/SQLite.hs | 2 - src/Simplex/Messaging/Agent/Protocol.hs | 15 +++-- src/Simplex/Messaging/Parsers.hs | 5 +- tests/AgentTests/FunctionalAPITests.hs | 2 + tests/SMPAgentClient.hs | 1 - tests/XFTPAgent.hs | 8 ++- 10 files changed, 105 insertions(+), 56 deletions(-) diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index 8da29d28b..9c0b3fe00 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -57,6 +57,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..)) import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..)) import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types +import qualified Simplex.FileTransfer.Types as FT import Simplex.FileTransfer.Util (removePath, uniqueCombine) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite @@ -71,6 +72,7 @@ import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String (strDecode, strEncode) import Simplex.Messaging.Protocol (EntityId, XFTPServer) +import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM) import System.FilePath (takeFileName, ()) import UnliftIO @@ -175,7 +177,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do runXFTPOperation cfg where runXFTPOperation :: AgentConfig -> AM () - runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = + runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpConsecutiveRetries} = withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case (RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _) -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (INTERNAL "chunk has no replicas") (fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays) -> do @@ -187,7 +189,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do where retryLoop loop e replicaDelay = do flip catchAgentError (\_ -> pure ()) $ do - when notifyOnRetry $ notify c rcvFileEntityId $ RFERR e + when (serverHostError e) $ notify c rcvFileEntityId $ RFWARN e liftIO $ closeXFTPServerClient c userId server digest withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay atomically $ assertAgentForeground c @@ -195,7 +197,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do retryDone = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM () downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do - unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ XFTP "" XFTP.NOT_APPROVED + unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ FILE NOT_APPROVED fsFileTmpPath <- lift $ toFSFilePath fileTmpPath chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest) @@ -236,7 +238,7 @@ withRetryIntervalLimit maxN ri action = retryOnError :: Text -> AM a -> AM a -> AgentErrorType -> AM a retryOnError name loop done e = do logError $ name <> " error: " <> tshow e - if temporaryAgentError e + if temporaryOrHostError e then loop else done @@ -272,7 +274,7 @@ runXFTPRcvLocalWorker c Worker {doWork} = do encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths when (FileDigest encDigest /= digest) $ throwE $ XFTP "" XFTP.DIGEST let destFile = CryptoFile fsSavePath cfArgs - void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile + void $ liftError (FILE . FILE_IO . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile case redirect of Nothing -> do notify c rcvFileEntityId $ RFDONE fsSavePath @@ -285,13 +287,13 @@ runXFTPRcvLocalWorker c Worker {doWork} = do atomically $ waitUntilForeground c withStore' c (`updateRcvFileComplete` rcvFileId) -- proceed with redirect - yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath) + yaml <- liftError (FILE . FILE_IO . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath) next@FileDescription {chunks = nextChunks} <- case strDecode (LB.toStrict yaml) of -- TODO switch to another error constructor - Left _ -> throwE . XFTP "" $ XFTP.REDIRECT "decode error" + Left _ -> throwE . FILE $ REDIRECT "decode error" Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest}) - | dstSize /= redirectSize -> throwE . XFTP "" $ XFTP.REDIRECT "size mismatch" - | dstDigest /= redirectDigest -> throwE . XFTP "" $ XFTP.REDIRECT "digest mismatch" + | dstSize /= redirectSize -> throwE . FILE $ REDIRECT "size mismatch" + | dstDigest /= redirectDigest -> throwE . FILE $ REDIRECT "digest mismatch" | otherwise -> pure fd -- register and download chunks from the actual file withStore c $ \db -> updateRcvFileRedirect db redirectDbId next @@ -349,7 +351,7 @@ xftpSendDescription' c userId (ValidFileDescription fdDirect@FileDescription {si let directYaml = prefixPath "direct.yaml" cfArgs <- atomically $ CF.randomArgs g let file = CryptoFile directYaml (Just cfArgs) - liftError (INTERNAL . show) $ CF.writeFile file (LB.fromStrict $ strEncode fdDirect) + liftError (FILE . FILE_IO . show) $ CF.writeFile file (LB.fromStrict $ strEncode fdDirect) key <- atomically $ C.randomSbKey g nonce <- atomically $ C.randomCbNonce g fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce $ Just RedirectFileInfo {size, digest} @@ -377,11 +379,11 @@ runXFTPSndPrepareWorker c Worker {doWork} = do runXFTPOperation cfg@AgentConfig {sndFilesTTL} = withWork c doWork (`getNextSndFileToPrepare` sndFilesTTL) $ \f@SndFile {sndFileId, sndFileEntityId, prefixPath} -> - prepareFile cfg f `catchAgentError` (sndWorkerInternalError c sndFileId sndFileEntityId prefixPath . show) + prepareFile cfg f `catchAgentError` sndWorkerInternalError c sndFileId sndFileEntityId prefixPath prepareFile :: AgentConfig -> SndFile -> AM () prepareFile _ SndFile {prefixPath = Nothing} = throwE $ INTERNAL "no prefix path" - prepareFile cfg sndFile@SndFile {sndFileId, userId, prefixPath = Just ppath, status} = do + prepareFile cfg sndFile@SndFile {sndFileId, sndFileEntityId, userId, prefixPath = Just ppath, status} = do SndFile {numRecipients, chunks} <- if status /= SFSEncrypted -- status is SFSNew or SFSEncrypting then do @@ -406,17 +408,17 @@ runXFTPSndPrepareWorker c Worker {doWork} = do let CryptoFile {filePath} = srcFile fileName = takeFileName filePath fileSize <- liftIO $ fromInteger <$> CF.getFileContentsSize srcFile - when (fileSize > maxFileSizeHard) $ throwE $ INTERNAL "max file size exceeded" + when (fileSize > maxFileSizeHard) $ throwE $ FILE FT.SIZE let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing} fileSize' = fromIntegral (B.length fileHdr) + fileSize payloadSize = fileSize' + fileSizeLen + authTagSize chunkSizes <- case redirect of Nothing -> pure $ prepareChunkSizes payloadSize Just _ -> case singleChunkSize payloadSize of - Nothing -> throwE $ INTERNAL "max file size exceeded for redirect" + Nothing -> throwE $ FILE FT.SIZE Just chunkSize -> pure [chunkSize] let encSize = sum $ map fromIntegral chunkSizes - void $ liftError (INTERNAL . show) $ encryptFile srcFile fileHdr key nonce fileSize' encSize fsEncPath + void $ liftError (FILE . FILE_IO . show) $ encryptFile srcFile fileHdr key nonce fileSize' encSize fsEncPath digest <- liftIO $ LC.sha512Hash <$> LB.readFile fsEncPath let chunkSpecs = prepareChunkSpecs fsEncPath chunkSizes chunkDigests <- liftIO $ mapM getChunkDigest chunkSpecs @@ -430,24 +432,32 @@ runXFTPSndPrepareWorker c Worker {doWork} = do where tryCreate = do usedSrvs <- newTVarIO ([] :: [XFTPServer]) - withRetryInterval (riFast ri) $ \_ loop -> do + let AgentClient {xftpServers} = c + userSrvCount <- length <$> atomically (TM.lookup userId xftpServers) + withRetryIntervalCount (riFast ri) $ \n _ loop -> do liftIO $ waitForUserNetwork c + let triedAllSrvs = n > userSrvCount createWithNextSrv usedSrvs - `catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwE e) e + `catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop triedAllSrvs e) (throwE e) e where - retryLoop loop = atomically (assertAgentForeground c) >> loop + -- we don't do closeXFTPServerClient here to not risk closing connection for concurrent chunk upload + retryLoop loop triedAllSrvs e = do + flip catchAgentError (\_ -> pure ()) $ do + when (triedAllSrvs && serverHostError e) $ notify c sndFileEntityId $ SFWARN e + atomically $ assertAgentForeground c + loop createWithNextSrv usedSrvs = do deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId - when deleted $ throwE $ INTERNAL "file deleted, aborting chunk creation" + when deleted $ throwE $ FILE NO_FILE withNextSrv c userId usedSrvs [] $ \srvAuth -> do replica <- agentXFTPNewChunk c ch numRecipients' srvAuth pure (replica, srvAuth) -sndWorkerInternalError :: AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> AM () -sndWorkerInternalError c sndFileId sndFileEntityId prefixPath internalErrStr = do +sndWorkerInternalError :: AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> AgentErrorType -> AM () +sndWorkerInternalError c sndFileId sndFileEntityId prefixPath err = do lift . forM_ prefixPath $ removePath <=< toFSFilePath - withStore' c $ \db -> updateSndFileError db sndFileId internalErrStr - notify c sndFileEntityId $ SFERR $ INTERNAL internalErrStr + withStore' c $ \db -> updateSndFileError db sndFileId (show err) + notify c sndFileEntityId $ SFERR err runXFTPSndWorker :: AgentClient -> XFTPServer -> Worker -> AM () runXFTPSndWorker c srv Worker {doWork} = do @@ -458,9 +468,9 @@ runXFTPSndWorker c srv Worker {doWork} = do runXFTPOperation cfg where runXFTPOperation :: AgentConfig -> AM () - runXFTPOperation cfg@AgentConfig {sndFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = do + runXFTPOperation cfg@AgentConfig {sndFilesTTL, reconnectInterval = ri, xftpConsecutiveRetries} = do withWork c doWork (\db -> getNextSndChunkToUpload db srv sndFilesTTL) $ \case - SndFileChunk {sndFileId, sndFileEntityId, filePrefixPath, replicas = []} -> sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) "chunk has no replicas" + SndFileChunk {sndFileId, sndFileEntityId, filePrefixPath, replicas = []} -> sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) (INTERNAL "chunk has no replicas") fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do @@ -470,17 +480,17 @@ runXFTPSndWorker c srv Worker {doWork} = do where retryLoop loop e replicaDelay = do flip catchAgentError (\_ -> pure ()) $ do - when notifyOnRetry $ notify c sndFileEntityId $ SFERR e + when (serverHostError e) $ notify c sndFileEntityId $ SFWARN e liftIO $ closeXFTPServerClient c userId server digest withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay atomically $ assertAgentForeground c loop - retryDone e = sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) (show e) + retryDone = sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> AM () uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica fsFilePath <- lift $ toFSFilePath filePath - unlessM (doesFileExist fsFilePath) $ throwE $ INTERNAL "encrypted file doesn't exist on upload" + unlessM (doesFileExist fsFilePath) $ throwE $ FILE NO_FILE let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec atomically $ assertAgentForeground c agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec' @@ -624,7 +634,7 @@ runXFTPDelWorker c srv Worker {doWork} = do runXFTPOperation cfg where runXFTPOperation :: AgentConfig -> AM () - runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = do + runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpConsecutiveRetries} = do -- no point in deleting files older than rcv ttl, as they will be expired on server withWork c doWork (\db -> getNextDeletedSndChunkReplica db srv rcvFilesTTL) processDeletedReplica where @@ -637,7 +647,7 @@ runXFTPDelWorker c srv Worker {doWork} = do where retryLoop loop e replicaDelay = do flip catchAgentError (\_ -> pure ()) $ do - when notifyOnRetry $ notify c "" $ SFERR e + when (serverHostError e) $ notify c "" $ SFWARN e liftIO $ closeXFTPServerClient c userId server chunkDigest withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay atomically $ assertAgentForeground c diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 678d39d52..d72f9862b 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -223,10 +223,6 @@ data XFTPErrorType FILE_IO | -- | file sending or receiving timeout TIMEOUT - | -- | bad redirect data - REDIRECT {redirectError :: String} - | -- | cannot proceed with download from not approved relays without proxy - NOT_APPROVED | -- | internal server error INTERNAL | -- | used internally, never returned by the server (to be removed) @@ -236,11 +232,9 @@ data XFTPErrorType instance StrEncoding XFTPErrorType where strEncode = \case CMD e -> "CMD " <> bshow e - REDIRECT e -> "REDIRECT " <> bshow e e -> bshow e strP = "CMD " *> (CMD <$> parseRead1) - <|> "REDIRECT " *> (REDIRECT <$> parseRead A.takeByteString) <|> parseRead1 instance Encoding XFTPErrorType where @@ -258,8 +252,6 @@ instance Encoding XFTPErrorType where HAS_FILE -> "HAS_FILE" FILE_IO -> "FILE_IO" TIMEOUT -> "TIMEOUT" - REDIRECT err -> "REDIRECT " <> smpEncode err - NOT_APPROVED -> "NOT_APPROVED" INTERNAL -> "INTERNAL" DUPLICATE_ -> "DUPLICATE_" @@ -278,8 +270,6 @@ instance Encoding XFTPErrorType where "HAS_FILE" -> pure HAS_FILE "FILE_IO" -> pure FILE_IO "TIMEOUT" -> pure TIMEOUT - "REDIRECT" -> REDIRECT <$> _smpP - "NOT_APPROVED" -> pure NOT_APPROVED "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ _ -> fail "bad error type" diff --git a/src/Simplex/FileTransfer/Types.hs b/src/Simplex/FileTransfer/Types.hs index ba306a6c6..15dc672da 100644 --- a/src/Simplex/FileTransfer/Types.hs +++ b/src/Simplex/FileTransfer/Types.hs @@ -2,24 +2,33 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} module Simplex.FileTransfer.Types where +import qualified Data.Aeson.TH as J +import qualified Data.Attoparsec.ByteString.Char8 as A +import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Data.Word (Word32) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.FileTransfer.Client (XFTPChunkSpec (..)) import Simplex.FileTransfer.Description -import Simplex.Messaging.Agent.Protocol (RcvFileId, SndFileId) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (fromTextField_) -import Simplex.Messaging.Protocol +import Simplex.Messaging.Parsers +import Simplex.Messaging.Protocol (XFTPServer) import System.FilePath (()) +type RcvFileId = ByteString + +type SndFileId = ByteString + authTagSize :: Int64 authTagSize = fromIntegral C.authTagSize @@ -236,3 +245,35 @@ data DeletedSndChunkReplica = DeletedSndChunkReplica retries :: Int } deriving (Show) + +data FileErrorType + = -- | cannot proceed with download from not approved relays without proxy + NOT_APPROVED + | -- | max file size exceeded + SIZE + | -- | bad redirect data + REDIRECT {redirectError :: String} + | -- | file crypto error + FILE_IO {fileIOError :: String} + | -- | file not found or was deleted + NO_FILE + deriving (Eq, Show) + +instance StrEncoding FileErrorType where + strP = + A.takeTill (== ' ') + >>= \case + "NOT_APPROVED" -> pure NOT_APPROVED + "SIZE" -> pure SIZE + "REDIRECT" -> REDIRECT <$> (A.space *> textP) + "FILE_IO" -> FILE_IO <$> (A.space *> textP) + "NO_FILE" -> pure NO_FILE + _ -> fail "bad FileErrorType" + strEncode = \case + NOT_APPROVED -> "NOT_APPROVED" + SIZE -> "SIZE" + REDIRECT e -> "REDIRECT " <> encodeUtf8 (T.pack e) + FILE_IO e -> "FILE_IO " <> encodeUtf8 (T.pack e) + NO_FILE -> "NO_FILE" + +$(J.deriveJSON (sumTypeJSON id) ''FileErrorType) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 7bc638496..c550ba04a 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -148,6 +148,7 @@ import Data.Word (Word16) import Simplex.FileTransfer.Agent (closeXFTPAgent, deleteSndFileInternal, deleteSndFileRemote, deleteSndFilesInternal, deleteSndFilesRemote, startXFTPWorkers, toFSFilePath, xftpDeleteRcvFile', xftpDeleteRcvFiles', xftpReceiveFile', xftpSendDescription', xftpSendFile') import Simplex.FileTransfer.Description (ValidFileDescription) import Simplex.FileTransfer.Protocol (FileParty (..)) +import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.FileTransfer.Util (removePath) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 63aa652ac..ee2bb16cc 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -105,7 +105,6 @@ data AgentConfig = AgentConfig storedMsgDataTTL :: NominalDiffTime, rcvFilesTTL :: NominalDiffTime, sndFilesTTL :: NominalDiffTime, - xftpNotifyErrsOnRetry :: Bool, xftpConsecutiveRetries :: Int, xftpMaxRecipientsPerRequest :: Int, deleteErrorCount :: Int, @@ -176,7 +175,6 @@ defaultAgentConfig = storedMsgDataTTL = 21 * nominalDay, rcvFilesTTL = 2 * nominalDay, sndFilesTTL = nominalDay, - xftpNotifyErrsOnRetry = True, xftpConsecutiveRetries = 3, xftpMaxRecipientsPerRequest = 200, deleteErrorCount = 10, diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 447658cfe..0067d4ada 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -115,8 +115,6 @@ module Simplex.Messaging.Agent.Protocol cryptoErrToSyncState, ATransmission, ConnId, - RcvFileId, - SndFileId, ConfirmationId, InvitationId, MsgIntegrity (..), @@ -169,6 +167,7 @@ import Database.SQLite.Simple.ToField import Simplex.FileTransfer.Description import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Transport (XFTPErrorType) +import Simplex.FileTransfer.Types (FileErrorType) import Simplex.Messaging.Agent.QueryString import Simplex.Messaging.Client (ProxyClientError) import qualified Simplex.Messaging.Crypto as C @@ -352,9 +351,11 @@ data AEvent (e :: AEntity) where RFPROG :: Int64 -> Int64 -> AEvent AERcvFile RFDONE :: FilePath -> AEvent AERcvFile RFERR :: AgentErrorType -> AEvent AERcvFile + RFWARN :: AgentErrorType -> AEvent AERcvFile SFPROG :: Int64 -> Int64 -> AEvent AESndFile SFDONE :: ValidFileDescription 'FSender -> [ValidFileDescription 'FRecipient] -> AEvent AESndFile SFERR :: AgentErrorType -> AEvent AESndFile + SFWARN :: AgentErrorType -> AEvent AESndFile deriving instance Eq (AEvent e) @@ -420,9 +421,11 @@ data AEventTag (e :: AEntity) where RFDONE_ :: AEventTag AERcvFile RFPROG_ :: AEventTag AERcvFile RFERR_ :: AEventTag AERcvFile + RFWARN_ :: AEventTag AERcvFile SFPROG_ :: AEventTag AESndFile SFDONE_ :: AEventTag AESndFile SFERR_ :: AEventTag AESndFile + SFWARN_ :: AEventTag AESndFile deriving instance Eq (AEventTag e) @@ -470,9 +473,11 @@ aEventTag = \case RFPROG {} -> RFPROG_ RFDONE {} -> RFDONE_ RFERR {} -> RFERR_ + RFWARN {} -> RFWARN_ SFPROG {} -> SFPROG_ SFDONE {} -> SFDONE_ SFERR {} -> SFERR_ + SFWARN {} -> SFWARN_ data QueueDirection = QDRcv | QDSnd deriving (Eq, Show) @@ -1077,10 +1082,6 @@ connModeT = \case -- | SMP agent connection ID. type ConnId = ByteString -type RcvFileId = ByteString - -type SndFileId = ByteString - type ConfirmationId = ByteString type InvitationId = ByteString @@ -1316,6 +1317,8 @@ data AgentErrorType NTF {serverAddress :: String, ntfErr :: ErrorType} | -- | XFTP protocol errors forwarded to agent clients XFTP {serverAddress :: String, xftpErr :: XFTPErrorType} + | -- | XFTP agent errors + FILE {fileErr :: FileErrorType} | -- | SMP proxy errors PROXY {proxyServer :: String, relayServer :: String, proxyErr :: ProxyClientError} | -- | XRCP protocol errors forwarded to agent clients diff --git a/src/Simplex/Messaging/Parsers.hs b/src/Simplex/Messaging/Parsers.hs index 39cb0383c..6ad9f867d 100644 --- a/src/Simplex/Messaging/Parsers.hs +++ b/src/Simplex/Messaging/Parsers.hs @@ -24,7 +24,7 @@ import Database.SQLite.Simple (ResultError (..), SQLData (..)) import Database.SQLite.Simple.FromField (FieldParser, returnError) import Database.SQLite.Simple.Internal (Field (..)) import Database.SQLite.Simple.Ok (Ok (Ok)) -import Simplex.Messaging.Util ((<$?>)) +import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) import Text.Read (readMaybe) base64P :: Parser ByteString @@ -154,3 +154,6 @@ singleFieldJSON_ objectTag tagModifier = defaultJSON :: J.Options defaultJSON = J.defaultOptions {J.omitNothingFields = True} + +textP :: Parser String +textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 8d1247384..d52c12877 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -158,6 +158,8 @@ pGet' c skipWarn = do DISCONNECT {} -> pGet c ERR (BROKER _ NETWORK) -> pGet c MWARN {} | skipWarn -> pGet c + RFWARN {} | skipWarn -> pGet c + SFWARN {} | skipWarn -> pGet c _ -> pure t pattern CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> AEvent e diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index c5de1533b..3c9907c48 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -71,7 +71,6 @@ agentCfg = ntfCfg = defaultNTFClientConfig {qSize = 1, defaultTransport = (ntfTestPort, transport @TLS), networkConfig}, reconnectInterval = fastRetryInterval, persistErrorInterval = 1, - xftpNotifyErrsOnRetry = False, ntfWorkerDelay = 100, ntfSMPWorkerDelay = 100, caCertificateFile = "tests/fixtures/ca.crt", diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 4580652e2..37ec00199 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -20,15 +20,17 @@ import Data.Int (Int64) import Data.List (find, isSuffixOf) import Data.Maybe (fromJust) import SMPAgentClient (agentCfg, initAgentServers, testDB, testDB2, testDB3) +import SMPClient (xit'') import Simplex.FileTransfer.Client (XFTPClientConfig (..)) import Simplex.FileTransfer.Description (FileChunk (..), FileDescription (..), FileDescriptionURI (..), ValidFileDescription, fileDescriptionURI, kb, mb, qrSizeLimit, pattern ValidFileDescription) import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..)) import Simplex.FileTransfer.Transport (XFTPErrorType (AUTH)) +import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.Messaging.Agent (AgentClient, testProtocolServer, xftpDeleteRcvFile, xftpDeleteSndFileInternal, xftpDeleteSndFileRemote, xftpReceiveFile, xftpSendDescription, xftpSendFile, xftpStartWorkers) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, xftpCfg) -import Simplex.Messaging.Agent.Protocol (AEvent (..), AgentErrorType (..), BrokerErrorType (..), RcvFileId, SndFileId, noAuthSrv) +import Simplex.Messaging.Agent.Protocol (AEvent (..), AgentErrorType (..), BrokerErrorType (..), noAuthSrv) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs) import qualified Simplex.Messaging.Crypto.File as CF @@ -58,7 +60,7 @@ xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do it "should resume receiving file after restart" testXFTPAgentReceiveRestore it "should cleanup rcv tmp path after permanent error" testXFTPAgentReceiveCleanup it "should resume sending file after restart" testXFTPAgentSendRestore - xit "should cleanup snd prefix path after permanent error" testXFTPAgentSendCleanup + xit'' "should cleanup snd prefix path after permanent error" testXFTPAgentSendCleanup it "should delete sent file on server" testXFTPAgentDelete it "should resume deleting file after restart" testXFTPAgentDeleteRestore -- TODO when server is fixed to correctly send AUTH error, this test has to be modified to expect AUTH error @@ -475,7 +477,7 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do -- send file - should fail with AUTH error withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do runRight_ $ xftpStartWorkers sndr' (Just senderFiles) - ("", sfId', SFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + ("", sfId', SFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- sfGet sndr' sfId' `shouldBe` sfId From da620c388a853cb8612cc695aed5cff6900a916b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:50:15 +0400 Subject: [PATCH 092/125] xftp: start chunk upload after all chunks are prepared (#1191) --- src/Simplex/FileTransfer/Agent.hs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index 9c0b3fe00..890966888 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -38,7 +38,7 @@ import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Coerce (coerce) import Data.Composition ((.:)) -import Data.Either (rights) +import Data.Either (partitionEithers, rights) import Data.Int (Int64) import Data.List (foldl', partition, sortOn) import qualified Data.List.NonEmpty as L @@ -71,7 +71,7 @@ import qualified Simplex.Messaging.Crypto.File as CF import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String (strDecode, strEncode) -import Simplex.Messaging.Protocol (EntityId, XFTPServer) +import Simplex.Messaging.Protocol (EntityId, ProtocolServer, ProtocolType (..), XFTPServer) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM) import System.FilePath (takeFileName, ()) @@ -397,9 +397,14 @@ runXFTPSndPrepareWorker c Worker {doWork} = do getSndFile db sndFileId else pure sndFile let numRecipients' = min numRecipients maxRecipients + -- in case chunk preparation previously failed mid-way, some chunks may already be created - + -- here we split previously prepared chunks from the pending ones to then build full list of servers + let (pendingChunks, preparedSrvs) = partitionEithers $ map srvOrPendingChunk chunks -- concurrently? -- separate worker to create chunks? record retries and delay on snd_file_chunks? - forM_ (filter (\SndFileChunk {replicas} -> null replicas) chunks) $ createChunk numRecipients' + srvs <- forM pendingChunks $ createChunk numRecipients' + let allSrvs = S.fromList $ preparedSrvs <> srvs + lift $ forM_ allSrvs $ \srv -> getXFTPSndWorker True c (Just srv) withStore' c $ \db -> updateSndFileStatus db sndFileId SFSUploading where AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients, messageRetryInterval = ri} = cfg @@ -423,12 +428,16 @@ runXFTPSndPrepareWorker c Worker {doWork} = do let chunkSpecs = prepareChunkSpecs fsEncPath chunkSizes chunkDigests <- liftIO $ mapM getChunkDigest chunkSpecs pure (FileDigest digest, zip chunkSpecs $ coerce chunkDigests) - createChunk :: Int -> SndFileChunk -> AM () + srvOrPendingChunk :: SndFileChunk -> Either SndFileChunk (ProtocolServer 'PXFTP) + srvOrPendingChunk ch@SndFileChunk {replicas} = case replicas of + [] -> Left ch + SndFileChunkReplica {server} : _ -> Right server + createChunk :: Int -> SndFileChunk -> AM (ProtocolServer 'PXFTP) createChunk numRecipients' ch = do atomically $ assertAgentForeground c (replica, ProtoServerWithAuth srv _) <- tryCreate withStore' c $ \db -> createSndFileReplica db ch replica - lift . void $ getXFTPSndWorker True c (Just srv) + pure srv where tryCreate = do usedSrvs <- newTVarIO ([] :: [XFTPServer]) From bb1d31e459337f5d2de05f4495ff50d0a8788dff Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:42:51 +0400 Subject: [PATCH 093/125] remote ctrl: differentiate RCP error when trying to connect to unknown host identity (#1195) * remote ctrl: differentiate RCP error when trying to connect to unknown host identity * rename * refactor --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- src/Simplex/RemoteControl/Client.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Simplex/RemoteControl/Client.hs b/src/Simplex/RemoteControl/Client.hs index a21630454..de0cbce3b 100644 --- a/src/Simplex/RemoteControl/Client.hs +++ b/src/Simplex/RemoteControl/Client.hs @@ -304,7 +304,9 @@ connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca, logDebug "Session ended" catchRCError :: ExceptT RCErrorType IO a -> (RCErrorType -> ExceptT RCErrorType IO a) -> ExceptT RCErrorType IO a -catchRCError = catchAllErrors (RCEException . show) +catchRCError = catchAllErrors $ \e -> case fromException e of + Just (TLS.Terminated _ _ (TLS.Error_Protocol (_, _, TLS.UnknownCa))) -> RCEIdentity + _ -> RCEException $ show e {-# INLINE catchRCError #-} putRCError :: ExceptT RCErrorType IO a -> TMVar (Either RCErrorType b) -> ExceptT RCErrorType IO a From 7008b0803172629edabacaefd7ac832ebc7191b6 Mon Sep 17 00:00:00 2001 From: "M. Sarmad Qadeer" Date: Sun, 16 Jun 2024 00:52:41 +0500 Subject: [PATCH 094/125] smp-server: fix layout (#1196) * smp-server: fix layout * ws --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/smp-server/static/index.html | 17 +++++++++++++++++ apps/smp-server/static/media/style.css | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/apps/smp-server/static/index.html b/apps/smp-server/static/index.html index e3889de2a..e276ff596 100644 --- a/apps/smp-server/static/index.html +++ b/apps/smp-server/static/index.html @@ -15,6 +15,12 @@ status (receive/send): NONE/NONE1. request connectionfrom agent2. create Alice's SMP queuestatus: NEW/NONE3. out-of-band invitation4. accept connectionstatus: NONE/NEW5. secure Alice's SMP queuestatus: NONE/SECURED6. create Bob's SMP queuestatus: NEW/SECURED7. confirm Alice's SMP queuestatus: NEW/CONFIRMEDstatus: CONFIRMED/NEW8. secure Bob's SMP queuestatus: CONFIRMED/SECUREDstatus: CONFIRMED/CONFIRMED9. notify Aliceabout connection success(no HELLO needed in v6)status: ACTIVE/ACTIVE10. notify Bobabout connection successstatus: CONFIRMED/CONFIRMEDstatus: ACTIVE/ACTIVEcreateConnectionNEW: create SMP queueallow sender to secureIDS: SMP queue IDsINV: invitationto connectOOB: invitation to connectjoinConnection:via invitation infoSKEY: secure queue (this command needs to be proxied)NEW: create SMP queueallow sender to secureIDS: SMP queue IDsSEND: Bob's info without sender's key (SMP confirmation with reply queues)MSG: Bob's info withoutsender server keyACK: confirm messageCONF: connection request IDand Bob's infoallowConnection: accept connection request,send Alice's infoSKEY: secure queue (this command needs to be proxied)SEND: Alice's info without sender's server key (SMP confirmation without reply queues)CON: connectedMSG: Alice's info withoutsender's server keyINFO: Alice's infoACK: confirm messageCON: connected \ No newline at end of file diff --git a/protocol/diagrams/duplex-messaging/duplex-creating-v2.mmd b/protocol/diagrams/duplex-messaging/duplex-creating-v2.mmd deleted file mode 100644 index 09ee913ff..000000000 --- a/protocol/diagrams/duplex-messaging/duplex-creating-v2.mmd +++ /dev/null @@ -1,71 +0,0 @@ -sequenceDiagram - participant A as Alice - participant AA as Alice's
agent - participant AS as Alice's
server - participant BS as Bob's
server - participant BA as Bob's
agent - participant B as Bob - - note over AA, BA: status (receive/send): NONE/NONE - - note over A, AA: 1. request connection
from agent - A ->> AA: NEW: create
duplex connection - - note over AA, AS: 2. create Alice's SMP queue - AA ->> AS: NEW: create SMP queue - AS ->> AA: IDS: SMP queue IDs - note over AA: status: NEW/NONE - - AA ->> A: INV: invitation
to connect - - note over A, B: 3. out-of-band invitation - A ->> B: OOB: invitation to connect - - note over BA, B: 4. accept connection - B ->> BA: JOIN:
via invitation info - note over BA: status: NONE/NEW - - note over BA, BS: 5. create Bob's SMP queue - BA ->> BS: NEW: create SMP queue - BS ->> BA: IDS: SMP queue IDs - note over BA: status: NEW/NEW - - note over BA, AA: 6. establish Alice's SMP queue - BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation with reply queues) - note over BA: status: NEW/CONFIRMED - - AS ->> AA: MSG: Bob's info and
sender server key - note over AA: status: CONFIRMED/NONE - AA ->> AS: ACK: confirm message - AA ->> A: CONF: connection request ID
and Bob's info - A ->> AA: LET: accept connection request,
send Alice's info - AA ->> AS: KEY: secure queue - note over AA: status: SECURED/NONE - - AA ->> BS: SEND: Alice's info and sender's server key (SMP confirmation without reply queues) - note over AA: status: SECURED/CONFIRMED - - BS ->> BA: MSG: Alice's info and
sender's server key - note over BA: status: CONFIRMED/CONFIRMED - BA ->> B: INFO: Alice's info - BA ->> BS: ACK: confirm message - BA ->> BS: KEY: secure queue - note over BA: status: SECURED/CONFIRMED - - BA ->> AS: SEND: HELLO: only needs to be sent once in v2 - - note over BA: status: SECURED/ACTIVE - note over BA, B: 7a. notify Bob
about connection success - BA ->> B: CON: connected - - AS ->> AA: MSG: HELLO: Alice's agent
knows Bob can send - note over AA: status: SECURED/ACTIVE - AA ->> AS: ACK: confirm message - note over A, AA: 7a. notify Alice
about connection success - AA ->> A: CON: connected - - AA ->> BS: SEND: HELLO: only needs to be sent once in v2 - note over AA: status: ACTIVE/ACTIVE - BS ->> BA: MSG: HELLO: Bob's agent
knows Alice can send - note over BA: status: ACTIVE/ACTIVE - BA ->> BS: ACK: confirm message diff --git a/protocol/diagrams/duplex-messaging/duplex-creating.mmd b/protocol/diagrams/duplex-messaging/duplex-creating.mmd index 738bad325..5cddf6aa4 100644 --- a/protocol/diagrams/duplex-messaging/duplex-creating.mmd +++ b/protocol/diagrams/duplex-messaging/duplex-creating.mmd @@ -8,8 +8,8 @@ sequenceDiagram note over AA, BA: status (receive/send): NONE/NONE - note over A, AA: 1. request connection from agent - A ->> AA: NEW: create
duplex connection + note over A, AA: 1. request connection
from agent + A ->> AA: createConnection note over AA, AS: 2. create Alice's SMP queue AA ->> AS: NEW: create SMP queue @@ -17,63 +17,58 @@ sequenceDiagram note over AA: status: NEW/NONE AA ->> A: INV: invitation
to connect - note over AA: status: PENDING/NONE note over A, B: 3. out-of-band invitation A ->> B: OOB: invitation to connect note over BA, B: 4. accept connection - B ->> BA: JOIN:
via invitation info + B ->> BA: joinConnection:
via invitation info note over BA: status: NONE/NEW - note over BA, AA: 5. establish Alice's SMP queue - BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation) - note over BA: status: NONE/CONFIRMED - activate BA + note over BA, BS: 5. create Bob's SMP queue + BA ->> BS: NEW: create SMP queue + BS ->> BA: IDS: SMP queue IDs + note over BA: status: NEW/NEW + + note over BA, AA: 6. confirm Alice's SMP queue + BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation with reply queues) + note over BA: status: NEW/CONFIRMED + AS ->> AA: MSG: Bob's info and
sender server key note over AA: status: CONFIRMED/NONE AA ->> AS: ACK: confirm message AA ->> A: CONF: connection request ID
and Bob's info - A ->> AA: LET: accept connection request,
send Alice's info + A ->> AA: allowConnection: accept connection request,
send Alice's info AA ->> AS: KEY: secure queue note over AA: status: SECURED/NONE - BA ->> AS: SEND: HELLO: try sending until successful - deactivate BA - note over BA: status: NONE/ACTIVE - AS ->> AA: MSG: HELLO: Alice's agent
knows Bob can send - note over AA: status: ACTIVE/NONE - AA ->> AS: ACK: confirm message + AA ->> BS: SEND: Alice's info and sender's server key (SMP confirmation without reply queues) + note over AA: status: SECURED/CONFIRMED - note over BA, BS: 6. create Bob's SMP queue - BA ->> BS: NEW: create SMP queue - BS ->> BA: IDS: SMP queue IDs - note over BA: status: NEW/ACTIVE - - note over AA, BA: 7. establish Bob's SMP queue - BA ->> AS: SEND: REPLY: invitation to the connect - note over BA: status: PENDING/ACTIVE - AS ->> AA: MSG: REPLY: invitation
to connect - note over AA: status: ACTIVE/NEW - AA ->> AS: ACK: confirm message - - AA ->> BS: SEND: Alice's info and sender's server key - note over AA: status: ACTIVE/CONFIRMED - activate AA + note over BA, AA: 7. confirm Bob's SMP queue BS ->> BA: MSG: Alice's info and
sender's server key - note over BA: status: CONFIRMED/ACTIVE + note over BA: status: CONFIRMED/CONFIRMED BA ->> B: INFO: Alice's info BA ->> BS: ACK: confirm message BA ->> BS: KEY: secure queue + note over BA: status: SECURED/CONFIRMED + + BA ->> AS: SEND: HELLO message + note over BA: status: SECURED/ACTIVE - AA ->> BS: SEND: HELLO: try sending until successful - deactivate AA + AS ->> AA: MSG: HELLO: Alice's agent
knows Bob can send + note over AA: status: SECURED/ACTIVE + AA ->> AS: ACK: confirm message + AA ->> BS: SEND: HELLO + + note over A, AA: 8. notify Alice
about connection success + AA ->> A: CON: connected note over AA: status: ACTIVE/ACTIVE + BS ->> BA: MSG: HELLO: Bob's agent
knows Alice can send note over BA: status: ACTIVE/ACTIVE BA ->> BS: ACK: confirm message - note over A, B: 8. notify users about connection success - AA ->> A: CON: connected + note over BA, B: 9. notify Bob
about connection success BA ->> B: CON: connected diff --git a/protocol/diagrams/duplex-messaging/duplex-creating.svg b/protocol/diagrams/duplex-messaging/duplex-creating.svg index 138935d3b..bea3bb23d 100644 --- a/protocol/diagrams/duplex-messaging/duplex-creating.svg +++ b/protocol/diagrams/duplex-messaging/duplex-creating.svg @@ -1 +1,3 @@ -AliceAlice'sagentAlice'sserverBob'sserverBob'sagentBobstatus (receive/send): NONE/NONE1. request connection from agentNEW: createduplex connection2. create Alice's SMP queueNEW: create SMP queueIDS: SMP queue IDsstatus: NEW/NONEINV: invitationto connectstatus: PENDING/NONE3. out-of-band invitationOOB: invitation to connect4. accept connectionJOIN:via invitation infostatus: NONE/NEW5. establish Alice's SMP queueSEND: Bob's info and sender server key (SMP confirmation)status: NONE/CONFIRMEDMSG: Bob's info andsender server keystatus: CONFIRMED/NONEACK: confirm messageREQ: connection request IDand Bob's infoACPT: accept connection request,send Alice's infoKEY: secure queuestatus: SECURED/NONESEND: HELLO: try sending until successfulstatus: NONE/ACTIVEMSG: HELLO: Alice's agentknows Bob can sendstatus: ACTIVE/NONEACK: confirm message6. create Bob's SMP queueNEW: create SMP queueIDS: SMP queue IDsstatus: NEW/ACTIVE7. establish Bob's SMP queueSEND: REPLY: invitation to the connectstatus: PENDING/ACTIVEMSG: REPLY: invitationto connectstatus: ACTIVE/NEWACK: confirm messageSEND: Alice's info and sender's server keystatus: ACTIVE/CONFIRMEDMSG: Alice's info andsender's server keystatus: CONFIRMED/ACTIVEINFO: Alice's infoACK: confirm messageKEY: secure queuestatus: SECURED/ACTIVESEND: HELLO: try sending until successfulstatus: ACTIVE/ACTIVEMSG: HELLO: Bob's agentknows Alice can sendstatus: ACTIVE/ACTIVEACK: confirm message8. notify users about connection successCON: connectedCON: connectedAliceAlice'sagentAlice'sserverBob'sserverBob'sagentBob \ No newline at end of file + + +BobBob'sagentBob'sserverAlice'sserverAlice'sagentAliceBobBob'sagentBob'sserverAlice'sserverAlice'sagentAlicestatus (receive/send): NONE/NONE1. request connectionfrom agent2. create Alice's SMP queuestatus: NEW/NONE3. out-of-band invitation4. accept connectionstatus: NONE/NEW5. secure Alice's SMP queuestatus: NONE/SECURED6. create Bob's SMP queuestatus: NEW/SECURED7. confirm Alice's SMP queuestatus: NEW/CONFIRMEDstatus: CONFIRMED/NEW8. secure Bob's SMP queuestatus: CONFIRMED/SECUREDstatus: CONFIRMED/CONFIRMED9. notify Aliceabout connection success(no HELLO needed in v6)status: ACTIVE/ACTIVE10. notify Bobabout connection successstatus: CONFIRMED/CONFIRMEDstatus: ACTIVE/ACTIVEcreateConnectionNEW: create SMP queueallow sender to secureIDS: SMP queue IDsINV: invitationto connectOOB: invitation to connectjoinConnection:via invitation infoSKEY: secure queue (this command needs to be proxied)NEW: create SMP queueallow sender to secureIDS: SMP queue IDsSEND: Bob's info without sender's key (SMP confirmation with reply queues)MSG: Bob's info withoutsender server keyACK: confirm messageCONF: connection request IDand Bob's infoallowConnection: accept connection request,send Alice's infoSKEY: secure queue (this command needs to be proxied)SEND: Alice's info without sender's server key (SMP confirmation without reply queues)CON: connectedMSG: Alice's info withoutsender's server keyINFO: Alice's infoACK: confirm messageCON: connected \ No newline at end of file diff --git a/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd b/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd new file mode 100644 index 000000000..75887dd0b --- /dev/null +++ b/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd @@ -0,0 +1,17 @@ +sequenceDiagram + participant A as Alice + participant R as Current server
that has A's
receive queue + participant R' as New server
that has the new A's
receive queue + participant S as Server
that has A's send queue
(B's receive queue) + participant B as Bob + + A ->> R': NEW: create new queue
(allow SKEY) + A ->> S: SEND: QADD (R'): send address
of the new queue(s) + S ->> B: MSG: QADD (R') + B ->> R': SKEY: secure new queue + B ->> R': SEND: QTEST + R' ->> A: MSG: QTEST + A ->> R: DEL: delete the old queue + B ->> R': SEND: send messages to the new queue + R' ->> A: MSG: receive messages from the new queue + \ No newline at end of file diff --git a/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg b/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg new file mode 100644 index 000000000..823074823 --- /dev/null +++ b/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg @@ -0,0 +1,3 @@ + + +BobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceBobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceNEW: create new queue(allow SKEY)SEND: QADD (R'): send addressof the new queue(s)MSG: QADD (R')SKEY: secure new queueSEND: QTESTMSG: QTESTDEL: delete the old queueSEND: send messages to the new queueMSG: receive messages from the new queue \ No newline at end of file diff --git a/protocol/diagrams/duplex-messaging/queue-rotation.mmd b/protocol/diagrams/duplex-messaging/queue-rotation.mmd new file mode 100644 index 000000000..9572a2c48 --- /dev/null +++ b/protocol/diagrams/duplex-messaging/queue-rotation.mmd @@ -0,0 +1,21 @@ +sequenceDiagram + participant A as Alice + participant R as Current server
that has A's
receive queue + participant R' as New server
that has the new A's
receive queue + participant S as Server
that has A's send queue
(B's receive queue) + participant B as Bob + + A ->> R': NEW: create new queue + A ->> S: SEND: QADD (R'): send address
of the new queue(s) + S ->> B: MSG: QADD (R') + B ->> R: SEND: QKEY (R'): sender's key
for the new queue(s) + R ->> A: MSG: QKEY(R') + A ->> R': KEY: secure new queue + A ->> S: SEND: QUSE (R'): instruction to use new queue(s) + S ->> B: MSG: QUSE (R') + B ->> R': SEND: QTEST + R' ->> A: MSG: QTEST + A ->> R: DEL: delete the old queue + B ->> R': SEND: send messages to the new queue + R' ->> A: MSG: receive messages from the new queue + \ No newline at end of file diff --git a/protocol/diagrams/duplex-messaging/queue-rotation.svg b/protocol/diagrams/duplex-messaging/queue-rotation.svg new file mode 100644 index 000000000..d477cd622 --- /dev/null +++ b/protocol/diagrams/duplex-messaging/queue-rotation.svg @@ -0,0 +1,3 @@ + + +BobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceBobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceNEW: create new queueSEND: QADD (R'): send addressof the new queue(s)MSG: QADD (R')SEND: QKEY (R'): sender's keyfor the new queue(s)MSG: QKEY(R')KEY: secure new queueSEND: QUSE (R'): instruction to use new queue(s)MSG: QUSE (R')SEND: QTESTMSG: QTESTDEL: delete the old queueSEND: send messages to the new queueMSG: receive messages from the new queue \ No newline at end of file diff --git a/protocol/diagrams/notifications/register-token-detailed.mmd b/protocol/diagrams/notifications/register-token-detailed.mmd new file mode 100644 index 000000000..bceef296f --- /dev/null +++ b/protocol/diagrams/notifications/register-token-detailed.mmd @@ -0,0 +1,30 @@ +sequenceDiagram + participant M as mobile app + participant C as chat core + participant A as agent + participant P as push server + participant APN as APN + + note over M, APN: get device token + M ->> APN: registerForRemoteNotifications() + APN ->> M: device token + + note over M, P: register device token with push server + M ->> C: /_ntf register + C ->> A: registerNtfToken() + A ->> P: TNEW + P ->> A: ID (tokenId) + A ->> C: registered + C ->> M: registered + + note over M, APN: verify device token + P ->> APN: E2E encrypted code
in background
notification + APN ->> M: deliver background notification with e2ee verification token + M ->> C: /_ntf verify + C ->> A: verifyNtfToken() + A ->> P: TVFY code + P ->> A: OK / ERR + A ->> C: verified + C ->> M: verified + + note over M, APN: now token ID can be used diff --git a/protocol/diagrams/notifications/register-token.mmd b/protocol/diagrams/notifications/register-token.mmd index bceef296f..caa05d716 100644 --- a/protocol/diagrams/notifications/register-token.mmd +++ b/protocol/diagrams/notifications/register-token.mmd @@ -1,30 +1,26 @@ sequenceDiagram - participant M as mobile app - participant C as chat core + participant C as client app participant A as agent - participant P as push server - participant APN as APN + participant P as SimpleX
Notification
Server + participant APN as Apple
Push Notifications
Server - note over M, APN: get device token - M ->> APN: registerForRemoteNotifications() - APN ->> M: device token + note over C, APN: get device token + C ->> APN: registerForRemoteNotifications() + APN ->> C: device token - note over M, P: register device token with push server - M ->> C: /_ntf register - C ->> A: registerNtfToken() + note over C, P: register device token with push server + C ->> A: registerToken A ->> P: TNEW P ->> A: ID (tokenId) A ->> C: registered - C ->> M: registered - note over M, APN: verify device token + note over C, APN: verify device token P ->> APN: E2E encrypted code
in background
notification - APN ->> M: deliver background notification with e2ee verification token - M ->> C: /_ntf verify - C ->> A: verifyNtfToken() + APN ->> C: deliver background notification with e2ee verification token + C ->> A: verifyToken
() A ->> P: TVFY code P ->> A: OK / ERR A ->> C: verified - C ->> M: verified - note over M, APN: now token ID can be used + note over C, APN: now token ID can be used + \ No newline at end of file diff --git a/protocol/diagrams/notifications/register-token.svg b/protocol/diagrams/notifications/register-token.svg new file mode 100644 index 000000000..662a185da --- /dev/null +++ b/protocol/diagrams/notifications/register-token.svg @@ -0,0 +1,3 @@ + + +ApplePush NotificationsServerSimpleXNotificationServeragentclient appApplePush NotificationsServerSimpleXNotificationServeragentclient appget device tokenregister device token with push serververify device tokennow token ID can be usedregisterForRemoteNotifications()device tokenregisterTokenTNEWID (tokenId)registeredE2E encrypted codein backgroundnotificationdeliver background notification with e2ee verification tokenverifyToken(<e2ee code>)TVFY codeOK / ERRverified \ No newline at end of file diff --git a/protocol/diagrams/notifications/subscription-detailed.mmd b/protocol/diagrams/notifications/subscription-detailed.mmd new file mode 100644 index 000000000..0db7cd7cc --- /dev/null +++ b/protocol/diagrams/notifications/subscription-detailed.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + participant M as mobile app + participant C as chat core + participant A as agent + participant S as SMP server + participant N as NTF server + participant APN as APN + + note over M, APN: register subscription + + alt register existing + M -->> A: on /_ntf register, for subscribed queues + else create new connection + A -->> S: NEW / JOIN + note over A, S: ...
Connection handshake
... + S -->> A: CON + end + A ->> S: NKEY nKey + S ->> A: NID nId + A ->> N: SNEW tknId dhKey (smpServer, nId, nKey) + N ->> A: ID subId dhKey + N ->> S: NSUB nId + S ->> N: OK [/ NMSG] + + note over M, APN: notify about message + + S ->> N: NMSG + N ->> APN: APNSMutableContent
ntfQueue, nonce + APN ->> M: UNMutableNotificationContent + note over M, S: ...
Client awaken, message is received
... + S ->> M: message + note over M: mutate notification + + note over M, APN: change APN token + + APN ->> M: new device token + M -->> C: /_ntf_sub update tkn + C -->> A: updateNtfToken() + A -->> N: TUPD tknId newDeviceToken + note over M, N: ...
Verify token
... diff --git a/protocol/diagrams/notifications/subscription.mmd b/protocol/diagrams/notifications/subscription.mmd index 0db7cd7cc..8b4db971c 100644 --- a/protocol/diagrams/notifications/subscription.mmd +++ b/protocol/diagrams/notifications/subscription.mmd @@ -1,17 +1,16 @@ sequenceDiagram - participant M as mobile app - participant C as chat core + participant C as client app participant A as agent participant S as SMP server participant N as NTF server participant APN as APN - note over M, APN: register subscription + note over C, APN: register subscription alt register existing - M -->> A: on /_ntf register, for subscribed queues + C -->> A: registerToken else create new connection - A -->> S: NEW / JOIN + A -->> S: create/joinConnection note over A, S: ...
Connection handshake
... S -->> A: CON end @@ -20,21 +19,20 @@ sequenceDiagram A ->> N: SNEW tknId dhKey (smpServer, nId, nKey) N ->> A: ID subId dhKey N ->> S: NSUB nId - S ->> N: OK [/ NMSG] + S ->> N: OK / NMSG:
confirm subscription - note over M, APN: notify about message + note over C, APN: notify about message S ->> N: NMSG N ->> APN: APNSMutableContent
ntfQueue, nonce - APN ->> M: UNMutableNotificationContent - note over M, S: ...
Client awaken, message is received
... - S ->> M: message - note over M: mutate notification + APN ->> C: UNMutableNotificationContent + note over C, S: ...
Client awaken, message is received
... + S ->> C: message + note over C: show notification - note over M, APN: change APN token + note over C, APN: change APN token - APN ->> M: new device token - M -->> C: /_ntf_sub update tkn - C -->> A: updateNtfToken() + APN ->> C: new device token + C -->> A: updateToken() A -->> N: TUPD tknId newDeviceToken - note over M, N: ...
Verify token
... + note over C, N: ...
Verify token
... diff --git a/protocol/diagrams/notifications/subscription.svg b/protocol/diagrams/notifications/subscription.svg new file mode 100644 index 000000000..49840ce58 --- /dev/null +++ b/protocol/diagrams/notifications/subscription.svg @@ -0,0 +1,3 @@ + + +APNNTF serverSMP serveragentclient appAPNNTF serverSMP serveragentclient appregister subscription...Connection handshake...alt[register existing][create new connection]notify about message...Client awaken, message is received...show notificationchange APN token...Verify token...registerTokencreate/joinConnectionCONNKEY nKeyNID nIdSNEW tknId dhKey (smpServer, nId, nKey)ID subId dhKeyNSUB nIdOK / NMSG:confirm subscriptionNMSGAPNSMutableContentntfQueue, nonceUNMutableNotificationContentmessagenew device tokenupdateToken()TUPD tknId newDeviceToken \ No newline at end of file diff --git a/protocol/diagrams/simplex-messaging/simplex-creating-fast.mmd b/protocol/diagrams/simplex-messaging/simplex-creating-fast.mmd new file mode 100644 index 000000000..48a59988d --- /dev/null +++ b/protocol/diagrams/simplex-messaging/simplex-creating-fast.mmd @@ -0,0 +1,23 @@ +sequenceDiagram + participant B as Bob (sender) + participant S as server (queue RID) + participant A as Alice (recipient) + + note over A: creating queue
("public" key RK
for msg retrieval) + A ->> S: 1. create queue ("NEW") + S ->> A: respond with queue RID and SID ("IDS") + + note over A: out-of-band msg
(sender's queue SID
and "public" key EK
to encrypt msgs) + A -->> B: 2. send out-of-band message + + note over B: secure queue
(with "public" key SK for
sending messages) + B ->> S: 3. confirm queue ("SKEY" command authorized with SK) + + note over B: confirm queue
(public key
for e2e encryption
and any optional
encrypted info.) + B ->> S: 4. confirm queue ("SEND" command authorized with SK) + + S ->> A: 5. deliver Bob's message (MSG) + note over A: decrypt message
("private" key EK) + A ->> S: acknowledge message (ACK) + + note over S: 6. simplex
queue RID
is ready to use! diff --git a/protocol/diagrams/simplex-messaging/simplex-creating-fast.svg b/protocol/diagrams/simplex-messaging/simplex-creating-fast.svg new file mode 100644 index 000000000..37337435f --- /dev/null +++ b/protocol/diagrams/simplex-messaging/simplex-creating-fast.svg @@ -0,0 +1,3 @@ + + +Alice (recipient)server (queue RID)Bob (sender)Alice (recipient)server (queue RID)Bob (sender)creating queue("public" key RKfor msg retrieval)out-of-band msg(sender's queue SIDand "public" key EKto encrypt msgs)secure queue(with "public" key SK forsending messages)confirm queue(public keyfor e2e encryptionand any optionalencrypted info.)decrypt message("private" key EK)6. simplexqueue RIDis ready to use!1. create queue ("NEW")respond with queue RID and SID ("IDS")2. send out-of-band message3. confirm queue ("SKEY" command authorized with SK)4. confirm queue ("SEND" command authorized with SK)5. deliver Bob's message (MSG)acknowledge message (ACK) \ No newline at end of file diff --git a/protocol/diagrams/simplex-messaging/simplex-creating.mmd b/protocol/diagrams/simplex-messaging/simplex-creating.mmd index 5116c3bed..f38c174e8 100644 --- a/protocol/diagrams/simplex-messaging/simplex-creating.mmd +++ b/protocol/diagrams/simplex-messaging/simplex-creating.mmd @@ -10,11 +10,13 @@ sequenceDiagram note over A: out-of-band msg
(sender's queue SID
and "public" key EK
to encrypt msgs) A -->> B: 2. send out-of-band message - note over B: confirm queue
("public" key SK for
sending messages
and any optional
info encrypted with
"public" key EK) + note over B: confirm queue
("public" key SK for
sending messages,
public key for
e2e encryption
and any optional
encrypted info) B ->> S: 3. confirm queue ("SEND" command not signed) - S ->> A: 4. deliver Bob's message + S ->> A: 4. deliver Bob's message (MSG) note over A: decrypt message
("private" key EK) + A ->> S: acknowledge message (ACK) + A ->> S: 5. secure queue ("KEY", RK-signed) note over S: 6. simplex
queue RID
is ready to use! diff --git a/protocol/diagrams/simplex-messaging/simplex-creating.svg b/protocol/diagrams/simplex-messaging/simplex-creating.svg index 325fbb6cf..33dd288b3 100644 --- a/protocol/diagrams/simplex-messaging/simplex-creating.svg +++ b/protocol/diagrams/simplex-messaging/simplex-creating.svg @@ -1 +1,3 @@ -Bob (sender)server (queue RID)Alice (recipient)creating queue("public" key RKfor msg retrieval)1. create queue ("NEW")respond with queue RID and SID ("IDS")out-of-band msg(sender's queue SIDand "public" key EKto encrypt msgs)2. send out-of-band messageconfirm queue("public" key SK forsending messagesand any optionalinfo encrypted with"public" key EK)3. confirm queue ("SEND" command not signed)4. deliver Bob's messagedecrypt message("private" key EK)5. secure queue ("KEY", RK-signed)6. simplexqueue RIDis ready to use!Bob (sender)server (queue RID)Alice (recipient) \ No newline at end of file + + +Alice (recipient)server (queue RID)Bob (sender)Alice (recipient)server (queue RID)Bob (sender)creating queue("public" key RKfor msg retrieval)out-of-band msg(sender's queue SIDand "public" key EKto encrypt msgs)confirm queue("public" key SK forsending messages,public key fore2e encryptionand any optionalencrypted info)decrypt message("private" key EK)6. simplexqueue RIDis ready to use!1. create queue ("NEW")respond with queue RID and SID ("IDS")2. send out-of-band message3. confirm queue ("SEND" command not signed)4. deliver Bob's message (MSG)acknowledge message (ACK)5. secure queue ("KEY", RK-signed) \ No newline at end of file diff --git a/protocol/diagrams/xftp/xftp-receiving-file.mmd b/protocol/diagrams/xftp/xftp-receiving-file.mmd new file mode 100644 index 000000000..d1944368b --- /dev/null +++ b/protocol/diagrams/xftp/xftp-receiving-file.mmd @@ -0,0 +1,18 @@ +sequenceDiagram + participant B as Bob (recipient) + participant S as XFTP server(s) + + note over B: having received file description
from sender + + loop for each chunk + B ->> S: 1a. download chunk ("FGET") + S ->> B: send chunk body ("FILE") + + opt + B ->> S: 1b. acknowledge chunk reception ("FACK") + note over S: delete recipient ID + S ->> B: respond with ok ("OK") + end + end + + note over B: 2. combine chunks into a file
3. decrypt file using key from file description
4. extract file name and unpad the file
5. validate file digest with the file description diff --git a/protocol/diagrams/xftp/xftp-receiving-file.svg b/protocol/diagrams/xftp/xftp-receiving-file.svg new file mode 100644 index 000000000..aceeb653d --- /dev/null +++ b/protocol/diagrams/xftp/xftp-receiving-file.svg @@ -0,0 +1,3 @@ + + +XFTP server(s)Bob (recipient)XFTP server(s)Bob (recipient)having received file descriptionfrom senderdelete recipient IDoptloop[for each chunk]2. combine chunks into a file3. decrypt file using key from file description4. extract file name and unpad the file5. validate file digest with the file description1a. download chunk ("FGET")send chunk body ("FILE")1b. acknowledge chunk reception ("FACK")respond with ok ("OK") \ No newline at end of file diff --git a/protocol/diagrams/xftp/xftp-sending-file.mmd b/protocol/diagrams/xftp/xftp-sending-file.mmd new file mode 100644 index 000000000..8e0b58498 --- /dev/null +++ b/protocol/diagrams/xftp/xftp-sending-file.mmd @@ -0,0 +1,23 @@ +sequenceDiagram + participant A as Alice (sender) + participant S as XFTP server(s) + participant B as recipient(s) + + note over A: 1. prepare file:
encrypt,
split into chunks,
generate recipient
keys, etc. + + loop for each chunk + A ->> S: 2a. register chunk ("FNEW") + S ->> A: respond with sender's and recipients' chunk IDs ("SIDS") + + opt + A ->> S: 2b. request additional recipient IDs ("FADD") + S ->> A: respond with added recipients' chunk IDs ("RIDS") + end + + A ->> S: 2c. upload chunk to chosen server ("FPUT") + S ->> A: respond with ok ("OK") + end + + note over A: 3. prepare file description(s) + + A -->> B: 4. send file description(s) out-of-band diff --git a/protocol/diagrams/xftp/xftp-sending-file.svg b/protocol/diagrams/xftp/xftp-sending-file.svg new file mode 100644 index 000000000..c3df658f5 --- /dev/null +++ b/protocol/diagrams/xftp/xftp-sending-file.svg @@ -0,0 +1,3 @@ + + +recipient(s)XFTP server(s)Alice (sender)recipient(s)XFTP server(s)Alice (sender)1. prepare file:encrypt,split into chunks,generate recipientkeys, etc.optloop[for each chunk]3. prepare file description(s)2a. register chunk ("FNEW")respond with sender's and recipients' chunk IDs ("SIDS")2b. request additional recipient IDs ("FADD")respond with added recipients' chunk IDs ("RIDS")2c. upload chunk to chosen server ("FPUT")respond with ok ("OK")4. send file description(s) out-of-band \ No newline at end of file diff --git a/protocol/diagrams/xrcp/session.mmd b/protocol/diagrams/xrcp/session.mmd new file mode 100644 index 000000000..2e98fbb1b --- /dev/null +++ b/protocol/diagrams/xrcp/session.mmd @@ -0,0 +1,44 @@ +sequenceDiagram + participant CI as Controller UI + participant CC as Controller Core + participant HC as Host Core + participant HI as Host UI + +note over CI, HI: 1. Session invitation +CI->>CC: "Link a mobile" +CC-->>CI: Session invitation URI +note over CC: Listen for TCP connection +activate CC +HI->>HC: Session invitation URI + +note over CI, HI: 2. Establishing TLS connection +HC-->>CC: TCP connect +note over CC, HC: TLS handshake +par + note over CC: validate client X509 credentials + CC->>CI: session code from tlsUnique + CI-->>CC: user confirmation +and + note over HC: validate server X509 credentials + HC->>HI: session code from tlsUnique + HI-->>HC: user confirmation +end + +note over CI, HI: 3. Session verification and protocol negotiation +HC->>CC: host HELLO +note over CC: validate version, CA fingerprint +alt + CC-->>HC: controller ERROR +else + CC-->>HC: controller HELLO + note over CC, HC: update stored keys +end +deactivate CC + +note over CI, HI: 4. Session operation +loop + CI->>CC: command + CC->>HC: XRCP command + HC-->>CC: XRCP response + CC-->>CI: response +end diff --git a/protocol/diagrams/xrcp/session.svg b/protocol/diagrams/xrcp/session.svg new file mode 100644 index 000000000..136b56fc6 --- /dev/null +++ b/protocol/diagrams/xrcp/session.svg @@ -0,0 +1 @@ +Host UIHost CoreController CoreController UIHost UIHost CoreController CoreController UI1. Session invitationListen for TCP connection2. Establishing TLS connectionTLS handshakevalidate client X509 credentialsvalidate server X509 credentialspar3. Session verification and protocol negotiationvalidate version, CA fingerprintupdate stored keysalt4. Session operationloop"Link a mobile"Session invitation URISession invitation URITCP connectsession code from tlsUniqueuser confirmationsession code from tlsUniqueuser confirmationhost HELLOcontroller ERRORcontroller HELLOcommandXRCP commandXRCP responseresponse \ No newline at end of file diff --git a/protocol/overview-tjr.md b/protocol/overview-tjr.md index 7f1e72768..d30f55235 100644 --- a/protocol/overview-tjr.md +++ b/protocol/overview-tjr.md @@ -1,4 +1,4 @@ -Revision 1, 2022-01-01 +Revision 2, 2024-06-22 Evgeny Poberezkin @@ -13,19 +13,23 @@ Evgeny Poberezkin - [Technical Details](#technical-details) - [Trust in Servers](#trust-in-servers) - [Client -> Server Communication](#client---server-communication) + - [2-hop Onion Message Routing](#2-hop-onion-message-routing) - [SimpleX Messaging Protocol](#simplex-messaging-protocol) - [SimpleX Agents](#simplex-agents) - [Encryption Primitives Used](#encryption-primitives-used) - [Threat model](#threat-model) - [Acknowledgements](#acknowledgements) + ## Introduction #### What is SimpleX SimpleX as a whole is a platform upon which applications can be built. [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) is one such application that also serves as an example and reference application. - - [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) (SMP) is a protocol to send messages in one direction to a recipient, relying on a server in-between. The messages are delivered via uni-directional queues created by recipients. + - [SimpleX Messaging Protocol](./simplex-messaging.md) (SMP) is a protocol to send messages in one direction to a recipient, relying on a server in-between. The messages are delivered via uni-directional queues created by recipients. + + - SMP protocol allows to send message via a SMP server playing proxy role using 2-hop onion routing (referred to as "private routing" in messaging clients) to protect transport information of the sender (IP address and session) from the server chosen (and possibly controlled) by the recipient. - SMP runs over a transport protocol (shown below as TLS) that provides integrity, server authentication, confidentiality, and transport channel binding. @@ -35,7 +39,9 @@ SimpleX as a whole is a platform upon which applications can be built. [SimpleX - SimpleX Client libraries speak SMP to SimpleX Servers and provide a low-level API not generally intended to be used by applications. - - SimpleX Agents interface with SimpleX Clients to provide a more high-level API intended to be used by applications. Typically they are embedded as libraries, but are designed so they can also be abstracted into local services. + - SimpleX Agents interface with SimpleX Clients to provide a more high-level API intended to be used by applications. Typically they are embedded as libraries, but can also be abstracted into local services. + + - SimpleX Agents communicate with other agents inside e2e encrypted envelopes provided by SMP protocol - the syntax and semantics of the messages exchanged by the agent are defined by [SMP agent protocol](./agent-protocol.md) *Diagram showing the SimpleX Chat app, with logical layers of the chat application interfacing with a SimpleX Agent library, which in turn interfaces with a SimpleX Client library. The Client library in turn speaks the Messaging Protocol to a SimpleX Server.* @@ -72,10 +78,11 @@ SimpleX as a whole is a platform upon which applications can be built. [SimpleX - Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency. -2. Provide better communication security and privacy than the alternative instant messaging solutions. In particular SimpleX provides better privacy of metadata (who talks to whom and when) and better security against active network attackers and malicious servers. +2. Provide better communication security and privacy than the alternative instant messaging solutions. In particular SimpleX provides better privacy of metadata (who talks to whom and when) and better security against active network attackers and malicious servers. 3. Balance user experience with privacy requirements, prioritizing experience of mobile device users. + #### In Comparison SimpleX network has a design similar to P2P networks, but unlike most P2P networks it consists of clients and servers without depending on any centralized component. @@ -91,53 +98,73 @@ In comparison to more traditional messaging applications (e.g. WhatsApp, Signal, - users can change servers with minimal disruption - even after an in-use server disappears, simply by changing the configuration on which servers the new queues are created. + ## Technical Details #### Trust in Servers Clients communicate directly with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides integrity, server authentication, confidentiality, and transport channel binding. By default, we assume this transport protocol is TLS. -Users use multiple servers, and choose where to receive their messages. Accordingly, they send messages to their communication partners' chosen servers. +Users use multiple servers, and choose where to receive their messages. Accordingly, they send messages to their communication partners' chosen servers either directly, if this is a known/trusted server, or via another SMP server providing proxy functionality to protect IP address and session of the sender. -Although end-to-end encryption is always present, users place a degree of trust in servers. This trust decision is very similar to a user's choice of email provider; however the trust placed in a SimpleX server is significantly less. Notably, there is no re-used identifier or credential between queues on the same (or different) servers. While a user *may* re-use a connection to fetch from multiple queues, or connect to a server from the same IP address, both are choices a user may opt into to break the promise of un-correlatable queues. +Although end-to-end encryption is always present, users place a degree of trust in servers they connect to. This trust decision is very similar to a user's choice of email provider; however the trust placed in a SimpleX server is significantly less. Notably, there is no re-used identifier or credential between queues on the same (or different) servers. While a user *may* re-use a transport connection to fetch messages from multiple queues, or connect to a server from the same IP address, both are choices a user may opt into to break the promise of un-correlatable queues. Users may trust a server because: -- They deploy and control the servers themselves from the available open-source code. This has the trade-offs of strong trust in the server but limited metadata obfuscation to a passive network observer. Techniques such as noise traffic, traffic mixing (incurring latency), and using an onion routing transport protocol can mitigate that latter. +- They deploy and control the servers themselves from the available open-source code. This has the trade-offs of strong trust in the server but limited metadata obfuscation to a passive network observer. Techniques such as noise traffic, traffic mixing (incurring latency), and using an onion routing transport protocol can mitigate that. - They use servers from a trusted commercial provider. The more clients the provider has, the less metadata about the communication times is leaked to the network observers. -- Users trust their contacts and the servers they chose. +By default, servers do not retain access logs, and permanently delete messages and queues when requested. Messages persist only in memory until they cross a threshold of time, typically on the order of days.[0] There is still a risk that a server maliciously records all queues and messages (even though encrypted) sent via the same transport connection to gain a partial knowledge of the user’s communications graph and other meta-data. -By default, servers do not retain access logs, and permanently delete messages and queues when requested. Messages persist only in memory until they cross a threshold of time, typically on the order of days.[0] There is still a risk that a server maliciously records all queues and messages (even though encrypted) sent via the same transport connection to gain a partial knowledge of the user’s communications graph and other meta-data. +SimpleX supports measures (managed transparently to the user at the agent level) to mitigate the trust placed in servers. These include rotating the queues in use between users, noise traffic, supporting overlay networks such as Tor, and isolating traffic to different queues to different transport connections (and Tor circuits, if Tor is used). -SimpleX supports measures (managed transparently to the user at the agent level) to mitigate the trust placed in servers. These include rotating the queues in use between users, noise traffic, and supporting overlay networks such as Tor. - -[0] While configurable by servers, a minimum value is enforced by the default software. SimpleX Agents provide redundant routing over queues to mitigate against message loss. +[0] While configurable by servers, a minimum value is enforced by the default software. SimpleX Agents can provide redundant routing over queues to mitigate against message loss. #### Client -> Server Communication Utilizing TLS grants the SimpleX Messaging Protocol (SMP) server authentication and metadata protection to a passive network observer. But SMP does not rely on the transport protocol for message confidentiality or client authentication. The SMP protocol itself provides end-to-end confidentiality, authentication, and integrity of messages between communicating parties. -Servers have long-lived, self-signed, offline certificates whose hash is pre-shared with clients over secure channels - either provided with the client library or provided in the secure introduction between clients. The offline certificate signs an online certificate used in the transport protocol handshake. [0] +Servers have long-lived, self-signed, offline certificates whose hash is pre-shared with clients over secure channels - either provided with the client library or provided in the secure introduction between clients, as part of the server address. The offline certificate signs an online certificate used in the transport protocol handshake. [0] If the transport protocol's confidentiality is broken, incoming and outgoing messages to the server cannot be correlated by message contents. Additionally, because of encryption at the SMP layer, impersonating the server is not sufficient to pass (and therefore correlate) a message from a sender to recipient - the only attack possible is to drop the messages. Only by additionally *compromising* the server can one pass and correlate messages. It's important to note that the SMP protocol does not do server authentication. Instead we rely upon the fact that an attacker who tricks the transport protocol into authenticating the server incorrectly cannot do anything with the SMP messages except drop them. -After the connection is established, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size to reduce metadata observable to a network adversary. The protocol has been designed to make traffic correlation attacks difficult, adapting ideas from Tor, remailers, and more general onion and mix networks. It does not try to replace Tor though - SimpleX servers can be deployed as onion services and SimpleX clients can communicate with servers over Tor to further improve participants privacy. +After the connection is established, the client sends blocks of a fixed size 16KB, and the server replies with the blocks of the same size to reduce metadata observable to a network adversary. The protocol has been designed to make traffic correlation attacks difficult, adapting ideas from Tor, remailers, and more general onion and mix networks. It does not try to replace Tor though - SimpleX servers can be deployed as onion services and SimpleX clients can communicate with servers over Tor to further improve participants privacy. -By using fixed-size blocks, oversized for the expected content, the vast majority of traffic is uniform in nature. When enough traffic is transiting a server simultaneously, the server acts as a (very) low-latency mix node. We can't rely on this behavior to make a security claim, but we have engineered to take advantage of it when we can. As mentioned, this holds true even if the transport connection is compromised. +By using fixed-size blocks, oversized for the expected content, the vast majority of traffic is uniform in nature. When enough traffic is transiting a server simultaneously, the server acts as a low-latency mix node. We can't rely on this behavior to make a security claim, but we have engineered to take advantage of it when we can. As mentioned, this holds true even if the transport connection is compromised. -The protocol does not protect against attacks targeted at particular users with known identities - e.g., if the attacker wants to prove that two known users are communicating, they can achieve it. At the same time, it substantially complicates large-scale traffic correlation, making determining the real user identities much less effective. +The protocol does not protect against attacks targeted at particular users with known identities - e.g., if the attacker wants to prove that two known users are communicating, they can achieve it by observing their local traffic. At the same time, it substantially complicates large-scale traffic correlation, making determining the real user identities much less effective. [0] Future versions of SMP may add support for revocation lists of certificates, presently this risk is mitigated by the SMP protocol itself. +#### 2-hop Onion Message Routing + +As SimpleX Messaging Protocol servers providing messaging queues are chosen by the recipients, in case senders connect to these servers directly the server owners (who potentially can be the recipients themselves) can learn senders' IP addresses (if Tor is not used) and which other queues on the same server are accessed by the user in the same transport connection (even if Tor is used). + +While the clients support isolating the messages sent to different queues into different transport connections (and Tor circuits), this is not practical, as it consumes additional traffic and system resources. + +To mitigate this problem SimpleX Messaging Protocol servers support 2-hop onion message routing when the SMP server chosen by the sender forwards the messages to the servers chosen by the recipients, thus protecting both the senders IP addresses and sessions, even if connection isolation and Tor are not used. + +The design of 2-hop onion message routing prevents these potential attacks: + +- MITM by proxy (SMP server that forwards the messages). + +- Identification by the proxy which and how many queues the sender sends messages to (as messages are additionally e2e encrypted between the sender and the destination SMP server). + +- Correlation of messages sent to different queues via the same user session (as random correlation IDs and keys are used for each message). + +See more details about 2-hop onion message routing design in [SimpleX Messaging Protocol](./simplex-messaging.md#proxying-sender-commands) + +Also see [Threat model](#threat-model) + + #### SimpleX Messaging Protocol -SMP is initialized with an in-person or out-of-band introduction message, where Alice provides Bob with details of a server (including IP, port, and hash of the long-lived offline certificate), a queue ID, and Alice's public key for her receiving queue. These introductions are similar to the PANDA key-exchange, in that if observed, the adversary can race to establish the communication channel instead of the intended participant. [0] +SMP is initialized with an in-person or out-of-band introduction message, where Alice provides Bob with details of a server (including IP address or host name, port, and hash of the long-lived offline certificate), a queue ID, and Alice's public keys to agree e2e encryption. These introductions are similar to the PANDA key-exchange, in that if observed, the adversary can race to establish the communication channel instead of the intended participant. [0] Because queues are uni-directional, Bob provides an identically-formatted introduction message to Alice over Alice's now-established receiving queue. @@ -145,6 +172,7 @@ When setting up a queue, the server will create separate sender and recipient qu [0] Users can additionally create public 'contact queues' that are only used to receive connection requests. + #### SimpleX Agents SimpleX agents provide higher-level operations compared to SimpleX Clients, who are primarily concerned with creating queues and communicating with servers using SMP. Agent operations include: @@ -157,9 +185,10 @@ SimpleX agents provide higher-level operations compared to SimpleX Clients, who - Noise traffic + #### Encryption Primitives Used -- Ed448 to sign/verify commands to SMP servers (Ed25519 is also supported via client/server configuration). +- Ed25519 or Curve25519 to authorize/verify commands to SMP servers (authorization algorithm is set via client/server configuration). - Curve25519 for DH exchange to agree: - the shared secret between server and recipient (to encrypt message bodies - it avoids shared cipher-text in sender and recipient traffic) - the shared secret between sender and recipient (to encrypt messages end-to-end in each queue - it avoids shared cipher-text in redundant queues). @@ -170,42 +199,44 @@ SimpleX agents provide higher-level operations compared to SimpleX Clients, who - AES-GCM AEAD cipher, - SHA512-based HKDF for key derivation. + ## Threat Model #### Global Assumptions - - A user protects their local database and key material - - The user's application is authentic, and no local malware is running - - The cryptographic primitives in use are not broken + - A user protects their local database and key material. + - The user's application is authentic, and no local malware is running. + - The cryptographic primitives in use are not broken. - A user's choice of servers is not directly tied to their identity or otherwise represents distinguishing information about the user. + - The user's client uses 2-hop onion message routing. #### A passive adversary able to monitor the traffic of one user *can:* - - identify that and when a user is using SimpleX + - identify that and when a user is using SimpleX. - - block SimpleX traffic - - - determine which servers the user communicates with + - determine which servers the user receives the messages from. - observe how much traffic is being sent, and make guesses as to its purpose. *cannot:* - - see who sends messages to the user and who the user sends the messages to + - see who sends messages to the user and who the user sends the messages to. + + - determine the servers used by users' contacts. #### A passive adversary able to monitor a set of senders and recipients *can:* - - identify who and when is using SimpleX + - identify who and when is using SimpleX. - - learn which SimpleX Messaging Protocol servers are used as receive queues for which users + - learn which SimpleX Messaging Protocol servers are used as receive queues for which users. - - learn when messages are sent and received + - learn when messages are sent and received. - - perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers + - perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers. - observe how much traffic is being sent, and make guesses as to its purpose @@ -217,43 +248,83 @@ SimpleX agents provide higher-level operations compared to SimpleX Clients, who *can:* -- learn when a queue recipient or sender is online +- learn when a queue recipient is online -- know how many messages are sent via the queue (although some may be noise) +- know how many messages are sent via the queue (although some may be noise or not content messages). -- perform queue correlation (matching multiple queues to a single user) via either a re-used transport connection, user's IP Address, or connection timing regularities +- learn which messages would trigger notifications even if a user does not use [push notifications](./push-notifications.md). -- learn a user's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, as long as Tor is not used. +- perform the correlation of the queue used to receive messages (matching multiple queues to a single user) via either a re-used transport connection, user's IP Address, or connection timing regularities. -- drop all future messages inserted into a queue, detectable only over other, redundant queues +- learn a recipient's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, as long as Tor is not used. + +- drop all future messages inserted into a queue, detectable only over other, redundant queues. - lie about the state of a queue to the recipient and/or to the sender (e.g. suspended or deleted when it is not). -- spam a user with invalid messages +- spam a user with invalid messages. *cannot:* -- undetectably add, duplicate, or corrupt individual messages +- undetectably add, duplicate, or corrupt individual messages. -- undetectably drop individual messages, so long as a subsequent message is delivered +- undetectably drop individual messages, so long as a subsequent message is delivered. -- learn the contents of messages +- learn the contents or type of messages. -- distinguish noise messages from regular messages except via timing regularities +- distinguish noise messages from regular messages except via timing regularities. -- compromise the user's end-to-end encryption with an active attack +- compromise the users' end-to-end encryption with an active attack. + +- learn a sender's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, even if Tor is not used (provided messages are sent via proxy SMP server). + +- perform senders' queue correlation (matching multiple queues to a single sender) via either a re-used transport connection, user's IP Address, or connection timing regularities, unless it has additional information from the proxy SMP server (provided messages are sent via proxy SMP server). + +#### SimpleX Messaging Protocol server that proxies the messages to another SMP server + +*can:* + +- learn a sender's IP address, as long as Tor is not used. + +- learn when a sender with a given IP address is online. + +- know how many messages are sent from a given IP address and to a given destination SMP server. + +- drop all messages from a given IP address or to a given destination server. + +- unless destination SMP server detects repeated public DH keys of senders, replay messages to a destination server within a single session, causing either duplicate message delivery (which will be detected and ignored by the receiving clients), or, when receiving client is not connected to SMP server, exhausting capacity of destination queues used within the session. + +*cannot:* + +- perform queue correlation (matching multiple queues to a single user), unless it has additional information from the destination SMP server. + +- undetectably add, duplicate, or corrupt individual messages. + +- undetectably drop individual messages, so long as a subsequent message is delivered. + +- learn the contents or type of messages. + +- learn which messages would trigger notifications. + +- learn the destination queues of messages. + +- distinguish noise messages from regular messages except via timing regularities. + +- compromise the user's end-to-end encryption with another user via an active attack. + +- compromise the user's end-to-end encryption with the destination SMP servers via an active attack. #### An attacker who obtained Alice's (decrypted) chat database *can:* -- see the history of all messages exchanged by Alice with her communication partners +- see the history of all messages exchanged by Alice with her communication partners. -- see shared profiles of contacts and groups +- see shared profiles of contacts and groups. -- surreptitiously receive new messages sent to Alice via existing queues; until communication queues are rotated or the Double-Ratchet advances forward +- surreptitiously receive new messages sent to Alice via existing queues; until communication queues are rotated or the Double-Ratchet advances forward. -- prevent Alice from receiving all new messages sent to her - either surreptitiously by emptying the queues regularly or overtly by deleting them +- prevent Alice from receiving all new messages sent to her - either surreptitiously by emptying the queues regularly or overtly by deleting them. - send messages from the user to their contacts; recipients will detect it as soon as the user sends the next message, because the previous message hash won’t match (and potentially won’t be able to decrypt them in case they don’t keep the previous ratchet keys). @@ -269,41 +340,41 @@ SimpleX agents provide higher-level operations compared to SimpleX Clients, who *can:* -- spam the user with messages +- spam the user with messages. -- forever retain messages from the user +- forever retain messages from the user. *cannot:* -- cryptographically prove to a third-party that a message came from a user (assuming the user’s device is not seized) +- cryptographically prove to a third-party that a message came from a user (assuming the user’s device is not seized). -- prove that two contacts they have is the same user +- prove that two contacts they have is the same user. -- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user +- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user. #### An attacker who observes Alice showing an introduction message to Bob *can:* - - Impersonate Bob to Alice + - Impersonate Bob to Alice. *cannot:* - - Impersonate Alice to Bob + - Impersonate Alice to Bob. #### An attacker with Internet access *can:* -- Denial of Service SimpleX messaging servers +- Denial of Service SimpleX messaging servers. -- spam a user's public “contact queue” with connection requests +- spam a user's public “contact queue” with connection requests. *cannot:* -- send messages to a user who they are not connected with +- send messages to a user who they are not connected with. -- enumerate queues on a SimpleX server +- enumerate queues on a SimpleX server. ## Acknowledgements diff --git a/protocol/pqdr.md b/protocol/pqdr.md new file mode 100644 index 000000000..27f7082c8 --- /dev/null +++ b/protocol/pqdr.md @@ -0,0 +1,222 @@ +Version 1, 2024-06-22 + +# Post-quantum resistant augmented double ratchet algorithm (PQDR) + +## Table of contents + +- [Overview](#overview) +- [Comparison with the other approaches](#comparison-with-the-other-approaches) + - [PQXDH for post-quantum key agreement](#pqxdh-for-post-quantum-key-agreement) (Signal) + - [Hybrid Signal protocol for post-quantum encryption](#hybrid-signal-protocol-for-post-quantum-encryption) (Tutanota) +- [Augmented double ratchet algorithm](#augmented-double-ratchet-algorithm) +- [Double ratchet with encrypted headers augmented with double PQ KEM](#double-ratchet-with-encrypted-headers-augmented-with-double-pq-kem) + - [Initialization](#initialization) + - [Encrypting messages](#encrypting-messages) + - [Decrypting messages](#decrypting-messages) +- [Implementation considerations](#implementation-considerations) +- [Chosen KEM algorithm](#chosen-kem-algorithm) +- [Summary](#summary) + +## Overview + +It is a reasonable assumption that "record-now-decrypt-later" attacks are ongoing, so the users want to use cryptographic schemes for end-to-end encryption that are augmented with some post-quantum algorithm that is believed to be resistant to quantum computers. + +SimpleX Chat uses [double-ratchet with header encryption](https://signal.org/docs/specifications/doubleratchet/#double-ratchet-with-header-encryption) to provide end-to-end encryption to messages and files. This document describes augmented algorithm with post-quantum key encapsulation mechanism (KEM) making it resistant to quantum computers. + +Double-ratchet algorithm is a state of the art solution for end to end encryption offering a set of qualities that is not present in any other algorithm: + +- perfect forward secrecy, i.e. compromise of session or long term keys does not lead to the ability to decrypt any of the past messages. +- deniability (also known as repudiation), i.e. the fact that the recipient of the message while having the proof of message authenticity, cannot prove to a third party that the sender actually sent this message. +- break-in recovery (also know as post-compromise security or future secrecy), i.e. the ability of the end-to-end encryption security to recover from the compromise of the long term keys. This is achieved by generating a new random key pair whenever a new DH key is received (DH ratchet step). + +It is desirable to preserve all these qualities when augmenting the algorithm with a post-quantum algorithm, and having these qualities resistant to both conventional and quantum computers. + +## Comparison with the other approaches + +### PQXDH for post-quantum key agreement + +[The solution](https://signal.org/docs/specifications/pqxdh/) recently [introduced by Signal](https://signal.org/blog/pqxdh/) augments the initial key agreement ([X3DH](https://signal.org/docs/specifications/x3dh/)) that is made prior to double ratchet algorithm. This is believed to provide protection from "record-now-decrypt-later" attack, but if the attacker at any point obtains long term keys from any of the devices, the break-in recovery will not be post-quantum resistant, and the attacker with quantum computer will be able to decrypt all the subsequent messages. + +### Hybrid Signal protocol for post-quantum encryption + +[The solution](https://eprint.iacr.org/2021/875.pdf) [proposed by Tutanota](https://tutanota.com/blog/posts/pqmail-update/) aims to preserve the break-in recovery property of double ratchet, but in doing so it: +- replaces rather than augments DH key agreement with post-quantum KEM mechanism, making it potentially vulnerable to conventional computers. +- adds signature to the DH ratchet step, to compensate for not keeping DH key agreement, but losing the deniability property for some of the messages. + +## Augmented double ratchet algorithm + +The double ratchet algorithm is augmented with post-quantum KEM mechanism, preserving all properties of the double ratchet algorithm. + +It is possible, because although double ratchet uses DH (which is a non-interactive key exchanges), it uses it "interactively", when the new DH keys are generated by both parties in turns. Parties of double-ratchet encrypted communication can run two post-quantum key encapsulation mechanisms in parallel with both DH and KEM key agreements in each DH ratchet step, making break-in recovery of double ratchet algorithm post-quantum resistant, without losing deniability or resistance to conventional computers. + +Specifically, [double ratchet with encrypted headers](https://signal.org/docs/specifications/doubleratchet/#double-ratchet-with-header-encryption) is augmented with some post-quantum key encapsulation mechanism (KEM) as described below. A possible algorithm for PQ KEM is [NTRU-prime](https://ntruprime.cr.yp.to), that is currently adopted in SSH and has available implementations. It is important though that the proposed scheme can be used with any PQ KEM algorithm. + +The downside of the scheme is its substantial size overhead, as the encapsulation key and encapsulated shared secret are added to the header of each message. For the algorithm described below NTRU-prime adds ~2-4kb to each message (depending on the key size and the chosen variant). See [this table](https://ntruprime.cr.yp.to/security.html) for key and ciphertext sizes and the assessment of the security level for various key sizes. + +It is possible to reduce size overhead by using only one KEM agreement and making only one of two ratchet steps providing post-quantum resistant break-in recovery. + +## Double ratchet with encrypted headers augmented with double PQ KEM + +Algorithm below assumes that in addition to shared secret from the initial key agreement, there will be an encapsulation key available from the party that published its keys (Bob). + +### Initialization + +The double ratchet initialization is defined in pseudo-code. This pseudo-code is identical to Signal algorithm specification except for that parts that add post-quantum key agreement. + +``` +// Alice obtained Bob's keys and initializes ratchet first +def RatchetInitAlicePQ2HE(state, SK, bob_dh_public_key, shared_hka, shared_nhkb, bob_pq_kem_encapsulation_key): + state.DHRs = GENERATE_DH() + state.DHRr = bob_dh_public_key + // below added for post-quantum KEM + state.PQRs = GENERATE_PQKEM() + state.PQRr = bob_pq_kem_encapsulation_key + state.PQRss = random // shared secret for KEM + state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret + // above added for KEM + // the next line augments DH key agreement with PQ shared secret + state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss) + state.CKr = None + state.Ns = 0 + state.Nr = 0 + state.PN = 0 + state.MKSKIPPED = {} + state.HKs = shared_hka + state.HKr = None + state.NHKr = shared_nhkb + +// Bob initializes ratchet second, having received Alice's connection request +def RatchetInitBobPQ2HE(state, SK, bob_dh_key_pair, shared_hka, shared_nhkb, bob_pq_kem_key_pair): + state.DHRs = bob_dh_key_pair + state.DHRr = None + // below added for KEM + state.PQRs = bob_pq_kem_key_pair + state.PQRr = None + state.PQRss = None + state.PQRct = None + // above added for KEM + state.RK = SK + state.CKs = None + state.CKr = None + state.Ns = 0 + state.Nr = 0 + state.PN = 0 + state.MKSKIPPED = {} + state.HKs = None + state.NHKs = shared_nhkb + state.HKr = None + state.NHKr = shared_hka +``` + +`GENERATE_PQKEM` generates decapsulation/encapsulation key pair. + +`PQKEM-ENC` is key encapsulation algorithm. + +Other than commented lines, the above adds parameters `bob_pq_kem_encapsulation_key` and `bob_pq_kem_key_pair` to the ratchet initialization. Otherwise it is identical to the original double ratchet initialization. + +### Encrypting messages + +``` +def RatchetEncryptPQ2HE(state, plaintext, AD): + state.CKs, mk = KDF_CK(state.CKs) + // encapsulation key from PQRs and encapsulated shared secret is added to header + header = HEADER_PQ2( + dh = state.DHRs.public, + kem = state.PQRs.public, // added for KEM #2 + ct = state.PQRct // added for KEM #1 + pn = state.PN, + n = state.Ns, + ) + enc_header = HENCRYPT(state.HKs, header) + state.Ns += 1 + return enc_header, ENCRYPT(mk, plaintext, CONCAT(AD, enc_header)) +``` + +Other than adding encapsulation key and encapsulated shared secret into the header, the above is identical to the original double ratchet message encryption step. + +### Decrypting messages + +``` +def RatchetDecryptPQ2HE(state, enc_header, ciphertext, AD): + plaintext = TrySkippedMessageKeysHE(state, enc_header, ciphertext, AD) + if plaintext != None: + return plaintext + header, dh_ratchet = DecryptHeader(state, enc_header) // DecryptHeader is the same as in double ratchet specification + if dh_ratchet: + SkipMessageKeysHE(state, header.pn) // SkipMessageKeysHE is the same as in double ratchet specification + DHRatchetPQ2HE(state, header) + SkipMessageKeysHE(state, header.n) + state.CKr, mk = KDF_CK(state.CKr) + state.Nr += 1 + return DECRYPT(mk, ciphertext, CONCAT(AD, enc_header)) + +// DecryptHeader is the same as in double ratchet specification +def DecryptHeader(state, enc_header): + header = HDECRYPT(state.HKr, enc_header) + if header != None: + return header, False + header = HDECRYPT(state.NHKr, enc_header) + if header != None: + return header, True + raise Error() + +def DHRatchetPQ2HE(state, header): + state.PN = state.Ns + state.Ns = 0 + state.Nr = 0 + state.HKs = state.NHKs + state.HKr = state.NHKr + state.DHRr = header.dh + // save new encapsulation key from header + state.PQRr = header.kem + // decapsulate shared secret from header - KEM #2 + ss = PQKEM-DEC(state.PQRs.private, header.ct) + // use decapsulated shared secret with receiving ratchet + state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss) + state.DHRs = GENERATE_DH() + // below is added for KEM + state.PQRs = GENERATE_PQKEM() // generate new PQ key pair + state.PQRss = random // shared secret for KEM + state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret KEM #1 + // above is added for KEM + // use new shared secret with sending ratchet + state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss) +``` + +`PQKEM-DEC` is key decapsulation algorithm. + +`DHRatchetPQ2HE` augments both DH agreements with decapsulated shared secret from the received header and with the new shared secret, respectively. The new shared secret together with the new encapsulation key are saved in the state and will be added to the header in the next sent message. + +Other than augmenting DH key agreements with the shared secrets from KEM, the above is identical to the original double ratchet DH ratchet step. + +It is worth noting that while DH agreements work as ping-pong, when the new received DH key is used for both DH agreements (and only the sent DH key is updated for the second DH key agreement), PQ KEM agreements in the proposed scheme work as a "parallel ping-pong", with two balls in play all the time (two KEM agreements run in parallel). + +## Implementation considerations for SimpleX Messaging Protocol + +As SimpleX Messaging Protocol pads messages to a fixed size, using 16kb transport blocks, the size increase introduced by this scheme can be compensated for by using ZSTD encryption of JSON bodies and image previews encoded as base64. While there may be some rare cases of random texts that would fail to compress, in all real scenarios it would not cause the message size reduction. + +Sharing the initial keys in case of SimpleX Chat it is equivalent to sharing the invitation link. As encapsulation key is large, it may be inconvenient to share it in the link in some contexts, e.g. when QR codes are used. + +It is possible to postpone sharing the encapsulation key until the first message from Alice (confirmation message in SMP protocol), the party sending connection request. The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm. + +Another consideration is pairwise ratchets in groups. Key generation in sntrup761 is quite slow - on slow devices it can be as slow as 10-20 keys per second, so using this primitive in groups larger than 10-20 members would result in slow performance. + +For backward compatibility the implementation must support adding PQ-resistant key agreement to the existing connections. + +It is also beneficial to support removing PQ-resistant key agreement from the connections that have them, e.g. as the group size grows. + +### Chosen KEM algorithm + +The implementation uses Streamlined NTRU-Prime 761 (sntrup761) that was also used for OpenSSH for a long time. + +It was chosen over ML-KEM (Kyber) standardized by NIST for several reasons: + +- sntrup761 was used in OpenSSH for a long period of time. +- ML-KEM standardization process raised [concerns](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/WFRDl8DqYQ4) [amongst](https://blog.cr.yp.to/20231003-countcorrectly.html) the experts. +- ML-KEM (if modified) is likely to have conflicts with the existing patents, unlike sntrup761. + +It was chosen over non-interactive CTIDH due to its slower implementation, and lack of optimized code for aarch64 CPUs used in mobile devices. + +## Summary + +If chosen PQ KEM proves secure against quantum computer attacks, then the proposed augmented double ratchet will also be secure against quantum computer attack, including break-in recovery property, while keeping deniability and forward secrecy, because the [same proof](https://eprint.iacr.org/2016/1013.pdf) as for double ratchet algorithm would hold here, provided chosen KEM is secure. diff --git a/protocol/push-notifications.md b/protocol/push-notifications.md new file mode 100644 index 000000000..6d5e1dea0 --- /dev/null +++ b/protocol/push-notifications.md @@ -0,0 +1,398 @@ +Version 2, 2024-06-22 + +# Overview of push notifications for SimpleX Messaging Servers + +## Table of contents + +- [Introduction](#introduction) +- [Participating servers](#participating-servers) +- [Register device token to receive push notifications](#register-device-token-to-receive-push-notifications) +- [Subscribe to connection notifications](#subscribe-to-connection-notifications) +- [SimpleX Notification Server protocol](#simplex-notification-server-protocol) + - [Register new notification token](#register-new-notification-token) + - [Verify notification token](#verify-notification-token) + - [Check notification token status](#check-notification-token-status) + - [Replace notification token](#replace-notification-token) + - [Delete notification token](#delete-notification-token) + - [Subscribe to periodic notifications](#subscribe-to-periodic-notifications) + - [Create SMP message notification subscription](#create-smp-message-notification-subscription) + - [Check notification subscription status](#check-notification-subscription-status) + - [Delete notification subscription](#delete-notification-subscription) + - [Error responses](#error-responses) +- [Threat model](#threat-model) + +## Introduction + +SimpleX Messaging servers already operate as push servers and deliver the messages to subscribed clients as soon as they are sent to the servers. + +The reason for push notifications is to support instant message notifications on iOS that does not allow background services. + +## Participating servers + +The diagram below shows which servers participate in message notification delivery. + +While push provider (e.g., APN) can learn how many notifications are delivered to the user, it cannot access message content, even encrypted, or any message metadata - the notifications are e2e encrypted between SimpleX Notification Server and the user's device. + +``` + User's iOS device Internet Servers +--------------------- . ------------------------ . ----------------------------- + . . + . . can be self-hosted now ++--------------+ . . +----------------+ +| SimpleX Chat | -------------- TLS --------------- | SimpleX | +| client |------> SimpleX Messaging Protocol (SMP) ------> | Messaging | ++--------------+ ---------------------------------- | Server | + ^ | . . +----------------+ + | | . . . . . | . . . + | | . . | V | + | | . . |SMP| TLS + | | . . | | | SimpleX + | | . . . . . V . . . NTF Server + | | . . +----------------------------------+ + | | . . | +---------------+ | + | | -------------- TLS --------------- | | SimpleX | can be | + | |-----------> Notification Server Protocol -----> | | Notifications | self-hosted | + | ---------------------------------- | | Subscriber | in the future | + | . . | +---------------+ | + | . . | | | + | . . | V | + | . . | +---------------+ | + | . . | | SimpleX | | + | . . | | Push | | + | . . | | Server | | + | . . | +---------------+ | + | . . +----------------------------------+ + | . . . . . | . . . + | . . | V | + | . . |SMP| TLS + | . . | | | + | . . . . . V . . . + | -------------- TLS --------------- +-----------------+ + |----------------- Notification delivery <-------| Apple PN server | + ---------------------------------- +-----------------+ + . . +``` + +## Register device token to receive push notifications + +This diagram shows the process of registering a device to receive PUSH notifications via Apple Push Notification (APN) servers. + +![Register device notification token](./diagrams/notifications/register-token.svg) + +## Subscribe to connection notifications + +This diagram shows the process of subscription to notifications, notification delivery and device token update. + +![Subscribe to notifications](./diagrams/notifications/subscription.svg) + +## SimpleX Notification Server protocol + +To manage notification subscriptions to SMP servers, SimpleX Notification Server provides an RPC protocol with a similar design to SimpleX Messaging Protocol server. + +This protocol sends requests and responses in a fixed size blocks of 512 bytes over TLS, uses the same [syntax of protocol transmissions](./simplex-messaging.md#smp-transmission-and-transport-block-structure) as SMP protocol, and has the same transport [handshake syntax](./simplex-messaging.md#transport-handshake) (except the server certificate is not included in the handshake). + +Protocol commands have this syntax: + +``` +ntfServerTransmission = +ntfServerCmd = newTokenCmd / verifyTokenCmd / checkTokenCmd / + replaceTokenCmd / deleteTokenCmd / cronCmd / + newSubCmd / checkSubCmd / deleteSubCmd +``` +### Register new notification token + +This command should be used after the client app obtains a token from push notifications provider to register the token with the server. + +Having received this command the server will deliver a test notification via the push provider to validate that the client has this token. + +The command syntax: + +```abnf +newTokenCmd = %s"TNEW" SP newToken +newToken = %s"T" deviceToken authPubKey clientDhPubKey +deviceToken = pushProvider tokenString +pushProvider = apnsDev / apnsProd / apnsNull +apnsDev = "AD" ; APNS token for development environment +apnsProd = "AP" ; APNS token for production environment +apnsNull = "AN" ; token that does not trigger any notification delivery - used for server testing +tokenString = shortString +authPubKey = length x509encoded ; Ed25519 key used to verify clients commands +clientDhPubKey = length x509encoded ; X25519 key to agree e2e encryption between the server and client +shortString = length *OCTET +length = 1*1 OCTET +``` + +The server response syntax: + +```abnf +tokenIdResp = %s"IDTKN" SP entityId serverDhPubKey +entityId = shortString +serverDhPubKey = length x509encoded ; X25519 key to agree e2e encryption between the server and client +``` + +### Verify notification token + +This command is used to verify the token after the device receives the test notification from the push provider. + +The command syntax: + +```abnf +verifyTokenCmd = %s"TVFY" SP regCode +regCode = shortString +``` + +The response to this command is `okResp` or `errorResp` + +```abnf +okResp = %s"OK" +``` + +### Check notification token status + +This command is used to check the token status: + +```abnf +checkTokenCmd = %s"TCHK" +``` + +The response to this command: + +```abnf +tokenStatusResp = %s"TKN" SP tokenStatus +tokenStatus = %s"NEW" / %s"REGISTERED" / %s"INVALID" / %s"CONFIRMED" / %s"ACTIVE" / %s"EXPIRED" +``` + +### Replace notification token + +This command should be used when push provider issues a new notification token. + +It happens when: +- the app data is migrated to another device. +- the app is re-installed on the same device. +- can happen periodically, at push provider discretion. + +This command allows to replace the token without re-registering and re-subscribing all notification subscriptions. + +Using this command triggers the same verification flow as registering a new token. + +The command syntax: + +```abnf +replaceTokenCmd = %s"TRPL" SP deviceToken +``` + +The response to this command is `okResp` or `errorResp`. + +### Delete notification token + +The command syntax: + +```abnf +deleteTokenCmd = %s"TDEL" +``` + +The response to this command is `okResp` or `errorResp`. + +After this command all message notification subscriptions will be removed and no more notifications will be sent. + +### Subscribe to periodic notifications + +This command enables or disables periodic notifications sent to the client device irrespective of message notifications. + +This is useful for two reasons: +- it provides better privacy from notification server, as while the server learns the device token, it doesn't learn anything else about user communications. +- it allows to receive messages when notifications were dropped by push provider, e.g. while the device was offline, or lost by notification server, e.g. while it was restarting. + +The command syntax: + +```abnf +cronCmd = %s"TCRN" SP interval +interval = 2*2 OCTET ; Word16, minutes +``` + +The interval for periodic notifications is set in minutes, with the minimum of 20 minutes. The client should pass `0` to disable periodic notifications. + +### Create SMP message notification subscription + +This command makes notification server subscribe to message notifications from SMP server and to deliver them to push provider: + +```abnf +newSubCmd = %s"SNEW" newSub +newSub = %s "S" tokenId smpServer notifierId notifierKey +tokenId = shortString ; returned in response to `TNEW` command +smpServer = smpServer = hosts port fingerprint +hosts = length 1*host +host = shortString +port = shortString +fingerprint = shortString +notifierId = shortString ; returned by SMP server in response to `NKEY` SMP command +notifierKey = length x509encoded ; private key used to authorize requests to subscribe to message notifications +``` + +The response syntax: + +```abnf +subIdResp = %s"IDSUB" SP entityId +``` + +### Check notification subscription status + +This command syntax: + +```abnf +checkSubCmd = %s"SCHK" +``` + +The response: + +```abnf +subStatusResp = %s"SUB" SP subStatus +subStatus = %s"NEW" / %s"PENDING" / ; e.g., after SMP server disconnect/timeout while ntf server is retrying to connect + %s"ACTIVE" / %s"INACTIVE" / %s"END" / ; if another server subscribed to notifications + %s"AUTH" / subErrStatus +subErrStatus = %s"ERR" SP shortString +``` + +### Delete notification subscription + +The command syntax: + +```abnf +deleteSubCmd = %s"SDEL" +``` + +The response to this command is `okResp` or `errorResp`. + +After this command no more message notifications will be sent from this queue. + +### Error responses + +All commands can return error response: + +```abnf +errorResp = %s"ERR" SP errorType +``` + +Where `errorType` has the same syntax as in [SimpleX Messaging Protocol](./simplex-messaging.md#error-responses) + +## Threat Model + +This threat model compliments SimpleX Messaging Protocol [threat model](./overview-tjr.md#threat-model) + +#### A passive adversary able to monitor the traffic of one user + +*can:* + + - identify that and a user is using SimpleX push notifications. + +*cannot:* + + - determine which servers a user subscribed to the notifications from. + +#### A passive adversary able to monitor a set of senders and recipients + + *can:* + + - perform more efficient traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers. + +#### SimpleX Messaging Protocol server + +*can:* + +- learn which messages trigger push notifications. + +- learn IP address of SimpleX notification servers used by the user. + +- drop message notifications. + +- spam a user with invalid notifications. + +*cannot:* + +- learn user device token for push notifications. + +- learn which queues belong to the same users with any additional efficiency compared with not using push notifications. + +#### SimpleX Notification Server subscribed to message notifications + +*can:* + +- learn a user device token. + +- learn how many messaging queues and servers a user receives messages from. + +- learn how many message notifications are delivered to the user from each queue. + +- undetectably drop notifications. + +- spam a user with background notifications. + +*cannot:* + +- learn queue addresses for receiving or sending messages. + +- learn the contents or type of messages (not even encrypted). + +- learn anything about messages sent without notification flag. + +- spam a user with visible notifications (provided the client app can filter push notifications). + +- add, duplicate, or corrupt individual messages that will be shown to the user. + +#### SimpleX Notification Server subscribed ONLY to periodic notifications + +*can:* + +- learn a user device token. + +- drop periodic notifications. + +- spam a user with background notifications. + +*cannot:* + +- learn how many messaging queues and servers a user receives messages from. + +- learn how many message notifications are delivered to the user from each queue. + +- learn queue addresses for receiving or sending messages. + +- learn the contents or type of messages (not even encrypted). + +- learn anything about messages sent without notification flag. + +- spam a user with visible notifications (provided the client app can filter push notifications). + +- add, duplicate, or corrupt individual messages that will be shown to the user. + +#### A user’s contact + +*cannot:* + +- determine if a user uses push notifications or not. + +#### Push notification provider (e.g., APN) + +*can:* + +- learn that a user uses SimpleX app. + +- learn how many notifications are delivered to user's device. + +- drop notifications (in fact, APN coalesces notifications delivered while user's device is offline, delivering only the last one). + +*cannot:* + +- learn which SimpleX Messaging Protocol servers are used by a user (notifications are e2e encrypted). + +- learn which or how many messaging queues a user receives notifications from. + +- learn the contents or type of messages (not even encrypted, notifications only contain encrypted metadata). + +#### An attacker with Internet access + +*cannot:* + +- register notification token not present on attacker's device. + +- enumerate tokens or subscriptions on a SimpleX Notification Server. diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 48843cab3..16e4e6606 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -1,3 +1,5 @@ +Version 9, 2024-06-22 + # Simplex Messaging Protocol (SMP) ## Table of contents @@ -9,39 +11,51 @@ - [Simplex queue](#simplex-queue) - [SMP queue URI](#smp-queue-uri) - [SMP procedure](#smp-procedure) +- [Fast SMP procedure](#fast-smp-procedure) - [SMP qualities and features](#smp-qualities-and-features) - [Cryptographic algorithms](#cryptographic-algorithms) +- [Deniable client authentication scheme](#deniable-client-authentication-scheme) - [Simplex queue IDs](#simplex-queue-ids) - [Server security requirements](#server-security-requirements) - [Message delivery notifications](#message-delivery-notifications) -- [SMP Transmission structure](#smp-transmission-structure) +- [SMP Transmission and transport block structure](#smp-transmission-and-transport-block-structure) - [SMP commands](#smp-commands) - [Correlating responses with commands](#correlating-responses-with-commands) - - [Command authentication](#command-authentication) + - [Command verification](#command-verification) - [Keep-alive command](#keep-alive-command) - [Recipient commands](#recipient-commands) - [Create queue command](#create-queue-command) - [Subscribe to queue](#subscribe-to-queue) - - [Secure queue command](#secure-queue-command) + - [Secure queue by recipient](#secure-queue-by-recipient) - [Enable notifications command](#enable-notifications-command) - [Disable notifications command](#disable-notifications-command) + - [Get message command](#get-message-command) - [Acknowledge message delivery](#acknowledge-message-delivery) - [Suspend queue](#suspend-queue) - [Delete queue](#delete-queue) + - [Get queue state](#get-queue-state) - [Sender commands](#sender-commands) + - [Secure queue by sender](#secure-queue-by-sender) - [Send message](#send-message) + - [Proxying sender commands](#proxying-sender-commands) + - [Request proxied session](#request-proxied-session) + - [Send command via proxy](#send-command-via-proxy) + - [Forward command to destination server](#forward-command-to-destination-server) - [Notifier commands](#notifier-commands) - [Subscribe to queue notifications](#subscribe-to-queue-notifications) - [Server messages](#server-messages) - - [Queue IDs response](#queue-ids-response) - [Deliver queue message](#deliver-queue-message) - - [Notifier queue ID response](#notifier-queue-id-response) - [Deliver message notification](#deliver-message-notification) - [Subscription END notification](#subscription-end-notification) - [Error responses](#error-responses) - [OK response](#ok-response) -- [Appendices](#appendices) - - [Appendix A. Transport connection with the SMP server](#appendix-a) +- [Transport connection with the SMP server](#transport-connection-with-the-SMP-server) + - [General transport protocol considerations](#general-transport-protocol-considerations) + - [TLS transport encryption](#tls-transport-encryption) + - [Server certificate](#server-certificate) + - [ALPN to agree handshake version](#alpn-to-agree-handshake-version) + - [Transport handshake](#transport-handshake) + - [Additional transport privacy](#additional-transport-privacy) ## Abstract @@ -51,11 +65,11 @@ It's designed with the focus on communication security and integrity, under the It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system. -This document describes SMP protocol versions 3 and 4, the previous versions are discontinued. +This document describes SMP protocol versions 6 and 7, the previous versions are discontinued. ## Introduction -The objective of Simplex Messaging Protocol (SMP) is to facilitate the secure and private unidirectional transfer of messages from senders to recipients via persistent simplex queues managed by the message broker (server). +The objective of Simplex Messaging Protocol (SMP) is to facilitate the secure and private unidirectional transfer of messages from senders to recipients via persistent simplex queues managed by the message brokers (servers). SMP is independent of any particular transmission system and requires only a reliable ordered data stream channel. While this document describes transport over TCP, other transports are also possible. @@ -67,7 +81,7 @@ The protocol is designed with the focus on privacy and security, to some extent SMP does not use any form of participants' identities and provides [E2EE][2] without the possibility of [MITM attack][1] relying on two pre-requisites: -- the users can establish a secure encrypted transport connection with the SMP server. [Appendix A](#appendix-a) describes SMP transport protocol of such connection over TCP, but any other transport connection protocol can be used. +- the users can establish a secure encrypted transport connection with the SMP server. [Transport connection](#transport-connection-with-the-smp-server) section describes SMP transport protocol of such connection over TCP, but any other transport connection protocol can be used. - the recipient can pass a single message to the sender via a pre-existing secure and private communication channel (out-of-band message) - the information in this message is used to encrypt messages and to establish connection with SMP server. @@ -77,13 +91,13 @@ The SMP model has three communication participants: the recipient, the message b SMP server manages multiple "simplex queues" - data records on the server that identify communication channels from the senders to the recipients. The same communicating party that is the sender in one queue, can be the recipient in another - without exposing this fact to the server. -The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analyzing their queues in case SMP server is compromised. +The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to verify the recipient's and the sender's commands, provided by the clients. The users of SMP protocol must use a unique ephemeral keys for each queue, to prevent aggregating their queues by keys in case SMP server is compromised. Creating and using the queue requires sending commands to the SMP server from the recipient and the sender - they are described in detail in [SMP commands](#smp-commands) section. ## Out-of-band messages -The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share one or several [queue URIs](#smp-queue-uri) that parties can use to establish the initial connection, the encryption scheme and, it can include the public key(s) for end-to-end encryption. +The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share one or several [queue URIs](#smp-queue-uri) that parties can use to establish the initial connection, the encryption scheme, including the public key(s) for end-to-end encryption. The approach to out-of-band message passing and their syntax should be defined in application-level protocols. @@ -91,9 +105,9 @@ The approach to out-of-band message passing and their syntax should be defined i The simplex queue is the main unit of SMP protocol. It is used by: -- Sender of the queue (who received out-of-band message) to send messages to the server using sender's queue ID, signed by sender's key. +- Sender of the queue (who received out-of-band message) to send messages to the server using sender's queue ID, authorized by sender's key. -- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key. Recipient decrypts the messages with the key negotiated during the creation of the queue. +- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, authorizing the commands by the recipient key. Recipient decrypts the messages with the key negotiated during the creation of the queue. - Participant identities are not shared with the server - new unique keys and queue IDs are used for each queue. @@ -101,7 +115,7 @@ This simplex queue can serve as a building block for more complex communication This approach is based on the concept of [unidirectional networks][4] that are used for applications with high level of information security. -Access to each queue is controlled with unique (not shared with other queues) asymmetric key pairs, separate for the sender and the recipient. The sender and the receiver have private keys, and the server has associated public keys to authenticate participants' commands by verifying cryptographic signatures. +Access to each queue is controlled with unique (not shared with other queues) asymmetric key pairs, separate for the sender and the recipient. The sender and the receiver have private keys, and the server has associated public keys to authenticate participants' commands by verifying cryptographic authorizations. The messages sent over the queue are end-to-end encrypted using the DH secret agreed via out-of-band message and SMP confirmation. @@ -115,17 +129,21 @@ The protocol uses different IDs for sender and recipient in order to provide an ## SMP queue URI -The SMP queue URIs MUST include server identity, queue hostname, an optional port, sender queue ID and the public key that the clients must use to verify responses. Server identity is used to establish secure connection protected from MITM attack with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). +The SMP queue URIs MUST include server identity, queue hostname, an optional port, sender queue ID, and the recipient's public key to agree shared secret for e2e encryption, and an optional query string parameter `k=s` to indicate that the queue can be secured by the sender using `SKEY` command (see [Fast SMP procedure](#fast-smp-procedure) and [Secure queue by sender](#secure-queue-by-sender)). Server identity is used to establish secure connection protected from MITM attack with SMP server (see [Transport connection](#transport-connection-with-the-smp-server) for SMP transport protocol). The [ABNF][8] syntax of the queue URI is: ```abnf -queueURI = %s"smp://" smpServer "/" queueId "#" recipientDhPublicKey -smpServer = serverIdentity "@" srvHost [":" port] -srvHost = ; RFC1123, RFC5891 +queueURI = %s"smp://" smpServer "/" queueId "#/?" versionParam keyParam [sndSecureParam] +smpServer = serverIdentity "@" srvHosts [":" port] +srvHosts = ["," srvHosts] ; RFC1123, RFC5891 port = 1*DIGIT serverIdentity = base64url queueId = base64url +versionParam = %s"v=" versionRange +versionRange = 1*DIGIT / 1*DIGIT "-" 1*DIGIT +keyParam = %s"&dh=" recipientDhPublicKey +sndSecureParam = %s"&k=s" base64url = ; RFC4648, section 5 recipientDhPublicKey = x509UrlEncoded ; the recipient's Curve25519 key for DH exchange to derive the secret @@ -139,7 +157,7 @@ x509UrlEncoded = `port` is optional, the default TCP port for SMP protocol is 5223. -`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a)) +`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Transport connection](#transport-connection-with-the-smp-server)) ## SMP procedure @@ -149,17 +167,17 @@ To create and start using a simplex queue Alice and Bob follow these steps: 1. Alice creates a simplex queue on the server: - 1. Decides which SMP server to use (can be the same or different server that Alice uses for other queues) and opens secure encrypted transport connection to the chosen SMP server (see [Appendix A](#appendix-a)). + 1. Decides which SMP server to use (can be the same or different server that Alice uses for other queues) and opens secure encrypted transport connection to the chosen SMP server (see [Transport connection](#transport-connection-with-the-smp-server)). - 2. Generates a new random public/private key pair (encryption key - `EK`) that she did not use before for Bob to encrypt the messages. + 2. Generates a new random public/private key pair (encryption key - `EK`) that she did not use before to agree a shared secret with Bob to encrypt the messages. - 3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to sign commands and to decrypt the transmissions received from the server. + 3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to authorize commands to the server. 4. Generates one more random key pair (recipient DH key - `RDHK`) to negotiate symmetric key that will be used by the server to encrypt message bodies delivered to Alice (to avoid shared cipher-text inside transport connection). - 5. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" keys `RK` and `RDHK`. `RK` will be used to verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address). + 5. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" keys `RK` and `RDHK`. `RK` will be used by the server to verify the subsequent commands related to the same queue authorized by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unauthorized messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address). - 6. The server sends `"IDS"` response with queue IDs (`queueIds`): + 6. The server sends `IDS` response with queue IDs (`queueIds`): - Recipient ID `RID` for Alice to manage the queue and to receive the messages. @@ -167,27 +185,29 @@ To create and start using a simplex queue Alice and Bob follow these steps: - Server public DH key (`SDHK`) to negotiate a shared secret for message body encryption, that Alice uses to derive a shared secret with the server `SS`. -2. Alice sends an out-of-band message to Bob via the alternative channel that both Alice and Bob trust (see [protocol abstract](#simplex-messaging-protocol-abstract)). The message must include: +2. Alice sends an out-of-band message to Bob via the alternative channel that both Alice and Bob trust (see [protocol abstract](#simplex-messaging-protocol-abstract)). The message must include [SMP queue URI](#smp-queue-uri) with: - - Unique "public" key (`EK`) that Bob must use for E2E key agreement. + - Unique "public" key (`EK`) that Bob must use to agree a shared secret for E2E encryption. - - SMP server hostname and information to open secure encrypted transport connection (see [Appendix A](#appendix-a)). + - SMP server hostname and information to open secure encrypted transport connection (see [Transport connection](#transport-connection-with-the-smp-server)). - Sender queue ID `SID` for Bob to use. 3. Bob, having received the out-of-band message from Alice, connects to the queue: - 1. Generates a new random public/private key pair (sender key - `SK`) that he did not use before for him to sign messages sent to Alice's server. + 1. Generates a new random public/private key pair (sender key - `SK`) that he did not use before for him to authorize messages sent to Alice's server and another key pair for e2e encryption agreement. 2. Prepares the confirmation message for Alice to secure the queue. This message includes: - - Previously generated "public" key `SK` that will be used by Alice's server to authenticate Bob's messages, once the queue is secured. + - Previously generated "public" key `SK` that will be used by Alice's server to verify Bob's messages, once the queue is secured. + + - Public key to agree a shared secret with Alice for e2e encryption. - Optionally, any additional information (application specific, e.g. Bob's profile name and details). - 3. Encrypts the confirmation body with the "public" key `EK` (that Alice provided via the out-of-band message). + 3. Encrypts the confirmation body with the shared secret agreed using public key `EK` (that Alice provided via the out-of-band message). - 4. Sends the encrypted message to the server with queue ID `SID` (see `send` in [Send message](#send-message)). This initial message to the queue must not be signed - signed messages will be rejected until Alice secures the queue (below). + 4. Sends the encrypted message to the server with queue ID `SID` (see `send` in [Send message](#send-message)). This initial message to the queue must not be authorized - authorized messages will be rejected until Alice secures the queue (below). 4. Alice receives Bob's message from the server using recipient queue ID `RID` (possibly, via the same transport connection she already has opened - see `message` in [Deliver queue message](#deliver-queue-message)): @@ -195,13 +215,13 @@ To create and start using a simplex queue Alice and Bob follow these steps: 2. She decrypts received message with [key agreed with sender using] "private" key `EK`. - 3. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol. + 3. Anybody can send the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), so it's a "race" to secure the queue. Optionally, in the client application, Alice may identify Bob using the information provided, but it is out of scope of SMP protocol. 5. Alice secures the queue `RID` with `"KEY"` command so only Bob can send messages to it (see [Secure queue command](#secure-queue-command)): - 1. She sends the `KEY` command with `RID` signed with "private" key `RK` to update the queue to only accept requests signed by "private" key `SK` provided by Bob. This command contains unique "public" key `SK` previously generated by Bob. + 1. She sends the `KEY` command with `RID` signed with "private" key `RK` to update the queue to only accept requests authorized by "private" key `SK` provided by Bob. This command contains unique "public" key `SK` previously generated by Bob. - 2. From this moment the server will accept only signed commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has). + 2. From this moment the server will accept only authorized commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has). 3. Once queue is secured, Alice deletes `SID` and `SK` - even if Alice's client is compromised in the future, the attacker would not be able to send messages pretending to be Bob. @@ -217,19 +237,19 @@ Bob now can securely send messages to Alice: 1. Bob sends the message: - 1. He encrypts the message to Alice with "public" key `EK` (provided by Alice, only known to Bob, used only for one simplex queue). + 1. He encrypts the message to Alice with the agreed shared secret (using "public" key `EK` provided by Alice, only known to Bob, used only for one simplex queue). - 2. He signs `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue). + 2. He authorizes `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue). - 3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier received from Bob and provided to the server via `"KEY"` command). + 3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will verify using the "public" key `SK` (that Alice earlier received from Bob and provided to the server via `"KEY"` command). 2. Alice receives the message(s): - 1. She signs `"SUB"` command to the server to subscribe to the queue `RID` with the "private" key `RK` (see `subscribe` in [Subscribe to queue](#subscribe-to-queue)). + 1. She authorizes `"SUB"` command to the server to subscribe to the queue `RID` with the "private" key `RK` (see `subscribe` in [Subscribe to queue](#subscribe-to-queue)). - 2. The server, having authenticated Alice's command with the "public" key `RK` that she provided, delivers Bob's message(s) (see `message` in [Deliver queue message](#deliver-queue-message)). + 2. The server, having verified Alice's command with the "public" key `RK` that she provided, delivers Bob's message(s) (see `message` in [Deliver queue message](#deliver-queue-message)). - 3. She decrypts Bob's message(s) with the "private" key `EK` (that only she has). + 3. She decrypts Bob's message(s) with the shared secret agreed using "private" key `EK`. 4. She acknowledges the message reception to the server with `"ACK"` so that the server can delete the message and deliver the next messages. @@ -245,15 +265,27 @@ This flow is show on sequence diagram below. Sequence diagram does not show E2E encryption - server knows nothing about encryption between the sender and the receiver. -A higher level protocol application protocol should define the semantics that allow to use two simplex queues (or two sets of queues for redundancy) for the bi-directional or any other communication scenarios. +A higher level application protocol should define the semantics that allow to use two simplex queues (or two sets of queues for redundancy) for the bi-directional or any other communication scenarios. The SMP is intentionally unidirectional - it provides no answer to how Bob will know that the transmission succeeded, and whether Alice received any messages. There may be a scenario when Alice wants to securely receive the messages from Bob, but she does not want Bob to have any proof that she received any messages - this low-level protocol can be used in this scenario, as all Bob knows as a fact is that he was able to send one unsigned message to the server that Alice provided, and now he can only send messages signed with the key `SK` that he sent to the server - it does not prove that any message was received by Alice. -For practical purposes of bi-directional conversation, now that Bob can securely send encrypted messages to Alice, Bob can create the second simplex queue that will allow Alice to send messages to Bob in the same way, sending the second queue details via the first queue. If both Alice and Bob have their respective unique "public" keys (Alice's and Bob's `EK`s of two separate queues), or pass additional keys to sign the messages, the conversation can be both encrypted and signed. +For bi-directional conversation, now that Bob can securely send encrypted messages to Alice, Bob can create the second simplex queue that will allow Alice to send messages to Bob in the same way, sending the second queue details via the first queue. If both Alice and Bob have their respective unique "public" keys (Alice's and Bob's `EK`s of two separate queues), or pass additional keys to sign the messages, the conversation can be both encrypted and signed. The established queues can also be used to change the encryption keys providing [forward secrecy][5], or to negotiate using other SMP queue(s). -This protocol also can be used for off-the-record messaging, as Alice and Bob can use multiple queues between them and only information they pass to each other allows proving their identity, so if they want to share anything off-the-record they can initiate a new queue without linking it to any other information they exchanged. As a result, this protocol provides better anonymity and better protection from [MITM][1] than [OTR][6] protocol. +This protocol also can be used for off-the-record messaging, as Alice and Bob can use multiple queues between them and only information they pass to each other allows proving their identity, so if they want to share anything off-the-record they can initiate a new queue without linking it to any other information they exchanged. + +## Fast SMP procedure + +V9 of SMP protocol added support for creating messaging queue in fewer steps, with the sender being able to send the messages without waiting for the recipient to be online to secure the key. + +In step 1.5 of [SMP procedure](#smp-procedure) the client must use sndSecure parameter set to `T` (true) to allow sender securing the queue. + +In step 2, the [SMP queue URI](#smp-queue-uri) should include parameter indicating that the sender can secure the queue. + +In step 3.2, prior to sending the confirmation message Bob secures the queue using `SKEY` command. Confirmation message is now sent with sender authorization and Bob can continue sending the messages without Alice being online. This also allows faster negotiation of duplex connections. + +![Creating queue](./diagrams/simplex-messaging/simplex-creating-fast.svg) ## SMP qualities and features @@ -269,11 +301,11 @@ Simplex Messaging Protocol: - Multiple servers, that can be deployed by the system users, can be used to send and retrieve messages. - - Servers do not communicate with each other and do not "know" about other servers. + - Servers do not communicate with each other, except when used as proxy to forward commands to another server, and do not "know" about other servers. - Clients only communicate with servers (excluding the initial out-of-band message), so the message passing is asynchronous. - - For each queue, the message recipient defines the server through which the sender should send messages. + - For each queue, the message recipient defines the server through which the sender should send messages. To protect transport anonymity the sender can use their chosen server to forward commands to the server chosen by the recipient. - While multiple servers and multiple queues can be used to pass each message, it is in scope of application level protocol(s), and out of scope of this protocol. @@ -285,50 +317,56 @@ Simplex Messaging Protocol: - Each queue is created and managed by the queue recipient. - - Asymmetric encryption is used to sign and verify the requests to send and receive the messages. + - Asymmetric encryption is used to authorize and verify the requests to send and receive the messages. - - One unique "public" key is used by the servers to authenticate requests to send the messages into the queue, and another unique "public" key - to retrieve the messages from the queue. "Unique" here means that each "public" key is used only for one queue and is not used for any other context - effectively, this key is not public and does not represent any participant identity. + - One ephemeral public key is used by the servers to verify requests to send the messages into the queue, and another ephemeral public key - to verify requests to retrieve the messages from the queue. These ephemeral keys are used only for one queue, and are not used for any other context - this key does not represent any participant identity. - - Both recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is provided when the queue is secured. + - Both recipient and sender public keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is provided when the queue is secured. V9 of SMP protocol allows senders to provide their key to the server directly or via proxy, to avoid waiting until the recipient is online to secure the queue. - - The "public" keys known to the server and used to authenticate commands from the participants are unrelated to the keys used to encrypt and decrypt the messages - the latter keys are also unique per each queue but they are only known to participants, not to the servers. + - The "public" keys known to the server and used to verify commands from the participants are unrelated to the keys used to encrypt and decrypt the messages - the latter keys are also unique per each queue but they are only known to participants, not to the servers. - Messaging graph can be asymmetric: Bob's ability to send messages to Alice does not automatically lead to the Alice's ability to send messages to Bob. ## Cryptographic algorithms -Simplex messaging clients and servers must cryptographically sign commands, responses and messages for the following operations: +Simplex messaging clients must cryptographically authorize commands for the following operations: - With the recipient's key `RK` (server to verify): - create the queue (`NEW`) - subscribe to queue (`SUB`) - secure the queue (`KEY`) - enable queue notifications (`NKEY`) + - disable queue notifications (`NDEL`) - acknowledge received messages (`ACK`) - suspend the queue (`OFF`) - delete the queue (`DEL`) - With the sender's key `SK` (server to verify): + - secure queue (`SKEY`) - send messages (`SEND`) - With the optional notifier's key: - subscribe to message notifications (`NSUB`) -- With the server's key (for recipient and sender to verify) - - queue IDs response (`IDS`) - - notifier queue ID response (`NID`) - - delivered messages (`MSG`) - - `OK` and `ERR` responses (excluding error responses not related to a queue) -To sign/verify transmissions clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15]. +To authorize/verify transmissions clients and servers MUST use either signature algorithm Ed25519 algorithm defined in [RFC8709][15] or [deniable authentication scheme](#deniable-client-authentication-scheme) based on NaCL crypto_box. -To encrypt/decrypt message bodies delivered to the recipients, servers/clients MUST use x25519 or x448 algorithm defined in [RFC8709][15] to derive the shared secret (TODO encryption scheme). +It is recommended that clients use signature algorithm for the recipient commands and deniable authentication scheme for sender commands (to have non-repudiation quality in the whole protocol stack). -Clients MUST encrypt message bodies sent via SMP servers - the protocol for this end-to-end encryption should be chosen by the clients using SMP protocol. +To encrypt/decrypt message bodies delivered to the recipients, servers/clients MUST use NaCL crypto_box. -The reasons to use these algorithms: +Clients MUST encrypt message bodies sent via SMP servers using use NaCL crypto_box. -- Faster operation than RSA algorithms. -- DH key exchange provides forward secrecy. +## Deniable client authentication scheme -Future versions of the protocol may allow different cryptographic algorithms. +While e2e encryption algorithms used in the client applications have repudiation quality, which is the desirable default, using signature algorithm for command authorization has non-repudiation quality. + +SMP protocol supports repudiable authenticators to authorize client commands. These authenticators use NaCl crypto_box that proves authentication and third party unforgeability and, unlike signature, provides repudiation guarantee. See [crypto_box docs](https://nacl.cr.yp.to/box.html). + +When queue is created or secured, the recipient would provide a DH key (X25519) to the server (either their own or received from the sender, in case of KEY command), and the server would provide its own random X25519 key per session in the handshake header. The authenticator is computed in this way: + +```abnf +transmission = authenticator authorized +authenticator = crypto_box(sha512(authorized), secret = dh(client long term queue key, server session key), nonce = correlation ID) +authorized = sessionIdentifier corrId queueId protocol_command ; same as the currently signed part of the transmission +``` ## Simplex queue IDs @@ -351,7 +389,7 @@ Simplex messaging server implementations MUST NOT create, store or send to any o ## Message delivery notifications -Supporting message delivery while the client mobile app is not running requires sending push notifications with the device token. All alternative mechanisms for background message delivery are unreliable, particularly on iOS platform. Obviously, supporting push notification delivery by simply subscribing to messages would reduce meta-data privacy as it allows to see all queues that a given device uses. +Supporting message delivery while the client mobile app is not running requires sending push notifications with the device token. All alternative mechanisms for background message delivery are unreliable, particularly on iOS platform. To protect the privacy of the recipients, there are several commands in SMP protocol that allow enabling and subscribing to message notifications from SMP queues, using separate set of "notifier keys" and via separate queue IDs - as long as SMP server is not compromised, these notifier queue IDs cannot be correlated with recipient or sender queue IDs. @@ -362,14 +400,14 @@ The clients can optionally instruct a dedicated push notification server to subs - `subscribeNotifications` (`"NSUB"`) - see [Subscribe to queue notifications](#subscribe-to-queue-notifications). - `messageNotification` (`"NMSG"`) - see [Deliver message notification](#deliver-message-notification). -[`SEND` command](#send-message) includes the notification flag to instruct SMP server whether to send the notification - this flag is forwarded to the recepient inside encrypted envelope, together with the timestamp and the message body, so even if TLS is compromised this flag cannot be used for traffic correlation. +[`SEND` command](#send-message) includes the notification flag to instruct SMP server whether to send the notification - this flag is forwarded to the recipient inside encrypted envelope, together with the timestamp and the message body, so even if TLS is compromised this flag cannot be used for traffic correlation. ## SMP Transmission and transport block structure Each transport block has a fixed size of 16384 bytes for traffic uniformity. -From SMP version 4 each block can contain multiple transmissions, version 3 blocks have 1 transmission. -Some parts of SMP transmission are padded to a fixed size; this padding is uniformly added as a word16 encoded in network byte order - see `paddedString` syntax. +Each block can contain multiple transmissions. +Some parts of SMP transmission are padded to a fixed size; the size of the unpadded string is prepended as a word16 encoded in network byte order - see `paddedString` syntax. In places where some part of the transmission should be padded, the syntax for `paddedNotation` is used: @@ -383,26 +421,7 @@ paddedNotation = ; paddedLength - required length after padding, including 2 bytes for originalLength ``` -Each transmission/block for SMP v3 between the client and the server must have this format/syntax: - -```abnf -paddedTransmission = -transmission = signature signed -signed = sessionIdentifier corrId queueId smpCommand -; corrId is required in client commands and server responses, -; it is empty in server notifications. -corrId = length *OCTET -queueId = length *OCTET -; empty queue ID is used with "create" command and in some server responses -signature = length *OCTET -; empty signature can be used with "send" before the queue is secured with secure command -; signature is always empty with "ping" and "serverMsg" -length = 1*1 OCTET -``` - -`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9] - -Transport block for SMP v4 has this syntax: +Transport block for SMP transmission between the client and the server must have this syntax: ```abnf paddedTransportBlock = @@ -410,6 +429,24 @@ transportBlock = transmissionCount transmissions transmissionCount = 1*1 OCTET ; equal or greater than 1 transmissions = transmissionLength transmission [transmissions] transmissionLength = 2*2 OCTET ; word16 encoded in network byte order + +transmission = authorization authorized +authorized = sessionIdentifier corrId entityId smpCommand +corrId = %x18 24*24 OCTET / %x0 "" + ; corrId is required in client commands and server responses, + ; it is empty (0-length) in server notifications. + ; %x18 is 24 - the random correlation ID must be 24 bytes as it is used as a nonce for NaCL crypto_box in some contexts. +entityId = shortString ; queueId or proxySessionId + ; empty entityId ID is used with "create" command and in some server responses +authorization = shortString ; signature or authenticator + ; empty authorization can be used with "send" before the queue is secured with secure command + ; authorization is always empty with "ping" and server responses +sessionIdentifier = "" ; +sessionIdentifierForAuth = shortString + ; sessionIdentifierForAuth MUST be included in authorized transmission body. + ; From v7 of SMP protocol but it is no longer used in the transmission to save space and fit more transmissions in the transport block. +shortString = length *OCTET ; length prefixed bytearray 0-255 bytes +length = 1*1 OCTET ``` ## SMP commands @@ -417,11 +454,16 @@ transmissionLength = 2*2 OCTET ; word16 encoded in network byte order Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a]. ```abnf -smpCommand = ping / recipientCmd / send / subscribeNotifications / serverMsg -recipientCmd = create / subscribe / secure / enableNotifications / disableNotifications / - acknowledge / suspend / delete +smpCommand = ping / recipientCmd / senderCommand / + proxyCommand / subscribeNotifications / serverMsg +recipientCmd = create / subscribe / rcvSecure / + enableNotifications / disableNotifications / getMessage + acknowledge / suspend / delete / getQueueInfo +senderCommand = send / sndSecure +proxyCommand = proxySession / proxyCommand / relayCommand serverMsg = queueIds / message / notifierId / messageNotification / - unsubscribed / ok / error + proxySessionKey / proxyResponse / relayResponse + unsubscribed / queueInfo/ ok / error ``` The syntax of specific commands and responses is defined below. @@ -432,9 +474,9 @@ The server should send `queueIds`, `error` and `ok` responses in the same order If the transport connection is closed before some responses are sent, these responses should be discarded. -### Command authentication +### Command verification -SMP servers must authenticate all transmissions (excluding `ping` and initial `send` commands) by verifying the client signatures. Command signature should be generated by applying the algorithm specified for the queue to the `signed` block of the transmission, using the key associated with the queue ID (recipient's, sender's or notifier's, depending on which queue ID is used). +SMP servers must verify all transmissions (excluding `ping` and initial `send` commands) by verifying the client authorizations. Command authorization should be generated by applying the algorithm specified for the queue to the `signed` block of the transmission, using the key associated with the queue ID (recipient's, sender's or notifier's, depending on which queue ID is used). ### Keep-alive command @@ -452,37 +494,42 @@ Sending any of the commands in this section (other than `create`, that is sent w #### Create queue command -This command is sent by the recipient to the SMP server to create a new queue. The syntax is: +This command is sent by the recipient to the SMP server to create a new queue. + +Servers SHOULD support basic auth with this command, to allow only server owners and trusted users to create queues on the destiation servers. + +The syntax is: ```abnf -create = %s"NEW " recipientSignaturePublicKey recipientDhPublicKey -recipientSignaturePublicKey = length x509encoded -; the recipient's Ed25519 or Ed448 public key to verify commands for this queue - +create = %s"NEW " recipientAuthPublicKey recipientDhPublicKey basicAuth subscribe sndSecure +recipientAuthPublicKey = length x509encoded +; the recipient's Ed25519 or X25519 public key to verify commands for this queue recipientDhPublicKey = length x509encoded ; the recipient's Curve25519 key for DH exchange to derive the secret ; that the server will use to encrypt delivered message bodies ; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305). +basicAuth = "0" / "1" shortString ; server password +subscribeMode = %s"S" / %s"C" ; S - create and subscribe, C - only create +sndSecure = %s"T" / %s"F" ; T - sender can secure the queue, from v9 x509encoded = - length = 1*1 OCTET ``` -If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs and public keys to sign all responses and messages and to encrypt delivered message bodies: +If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs and public key to encrypt delivered message bodies: ```abnf -queueIds = %s"IDS " recipientId senderId srvDhPublicKey +queueIds = %s"IDS " recipientId senderId srvDhPublicKey sndSecure serverDhPublicKey = length x509encoded ; the server's Curve25519 key for DH exchange to derive the secret ; that the server will use to encrypt delivered message bodies to the recipient -recipientId = length *OCTET ; 16-24 bytes -senderId = length *OCTET ; 16-24 bytes +recipientId = shortString ; 16-24 bytes +senderId = shortString ; 16-24 bytes ``` -Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened. +Once the queue is created, depending on `subscribeMode` parameter of `NEW` command the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. To start receiving the messages from the existing queue when the new transport connection is opened the client must use `subscribe` command. -`NEW` transmission MUST be signed using the private part of the `recipientSignaturePublicKey` – this verifies that the client has the private key that will be used to sign subsequent commands for this queue. +`NEW` transmission MUST be authorized using the private part of the `recipientAuthPublicKey` – this verifies that the client has the private key that will be used to authorize subsequent commands for this queue. `IDS` response transmission MUST be sent with empty queue ID (the third part of the transmission). @@ -500,19 +547,23 @@ The first message will be delivered either immediately or as soon as it is avail This transmission and its response MUST be signed. -#### Secure queue command +#### Secure queue by recipient + +This command is only used until v8 of SMP protocol. V9 uses [SKEY](#secure-queue-by-sender). This command is sent by the recipient to the server to add sender's key to the queue: ```abnf -secure = %s"KEY " senderSignaturePublicKey -senderSignaturePublicKey = length x509encoded -; the sender's Ed25519 or Ed448 key to verify SEND commands for this queue +rcvSecure = %s"KEY " senderAuthPublicKey +senderAuthPublicKey = length x509encoded +; the sender's Ed25519 or X25519 key to verify SEND commands for this queue ``` `senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command. -Once the queue is secured only signed messages can be sent to it. +Once the queue is secured only authorized messages can be sent to it. + +This command MUST be used in transmission with recipient queue ID. #### Enable notifications command @@ -521,7 +572,7 @@ This command is sent by the recipient to the server to add notifier's key to the ```abnf enableNotifications = %s"NKEY " notifierKey recipientNotificationDhPublicKey notifierKey = length x509encoded -; the notifier's Ed25519 or Ed448 public key public key to verify NSUB command for this queue +; the notifier's Ed25519 or X25519 public key to verify NSUB command for this queue recipientNotificationDhPublicKey = length x509encoded ; the recipient's Curve25519 key for DH exchange to derive the secret @@ -533,7 +584,7 @@ The server will respond with `notifierId` response if notifications were enabled ```abnf notifierId = %s"NID " notifierId srvNotificationDhPublicKey -notifierId = length *OCTET ; 16-24 bytes +notifierId = shortString ; 16-24 bytes srvNotificationDhPublicKey = length x509encoded ; the server's Curve25519 key for DH exchange to derive the secret ; that the server will use to encrypt notification metadata to the recipient (encryptedNMsgMeta in NMSG) @@ -555,17 +606,30 @@ The server must respond `ok` to this command if it was successful. Once notifier's credentials are removed server will no longer send "NMSG" for this queue to notifier. +#### Get message command + +The client can use this command to receive one message without subscribing to the queue. This command is used when processing push notifications. + +The client MUST NOT use `SUB` and `GET` command on the same queue in the same transport connection - doing so would create an error. + +```abnf +getMessage = %s"GET" +``` + #### Acknowledge message delivery The recipient should send the acknowledgement of message delivery once the message was stored in the client, to notify the server that the message should be deleted: ```abnf -acknowledge = %s"ACK" +acknowledge = %s"ACK" SP msgId +msgId = shortString ``` -Even if acknowledgement is not sent by the recipient, the server should limit the time of message storage, whether it was delivered to the recipient or not. +Client must send message ID to acknowledge a particular message - to prevent double acknowledgement (e.g., when command response times out) resulting in message being lost. If the message was not delivered or if the ID of the message does not match the last delivered message, the server SHOULD respond with `ERR NO_MSG` error. -Having received the acknowledgement, SMP server should immediately delete the message and then send the next available message or respond with `ok` if there are no more messages available in this simplex queue. +The server should limit the time the message is stored, even if the message was not delivered or if acknowledgement is not sent by the recipient. + +Having received the acknowledgement, SMP server should delete the message and then send the next available message or respond with `ok` if there are no more messages available in this simplex queue. #### Suspend queue @@ -593,10 +657,67 @@ All undelivered messages must be deleted as soon as this command is received, be delete = %s"DEL" ``` +#### Get queue state + +This command is used by the queue recipient to get the debugging information about the current state of the queue. + +The response to that command is `INFO`. + +```abnf +getQueueInfo = %s"QUE" +queueInfo = %s"INFO " info +info = +``` + +The format of queue information is implementation specific, and is not part of the specification. For information, [JTD schema][17] for queue information returned by the reference implementation of SMP server is: + +```json +{ + "properties": { + "qiSnd": {"type": "boolean"}, + "qiNtf": {"type": "boolean"}, + "qiSize": {"type": "uint16"} + }, + "optionalProperties": { + "qiSub": { + "properties": { + "qSubThread": {"enum": ["noSub", "subPending", "subThread", "prohibitSub"]} + }, + "optionalProperties": { + "qDelivered": {"type": "string", "metadata": {"description": "message ID"}} + } + }, + "qiMsg": { + "properties": { + "msgId": {"type": "string"}, + "msgTs": {"type": "timestamp"}, + "msgType": {"enum": ["message", "quota"]} + } + } + } +} +``` + ### Sender commands Currently SMP defines only one command that can be used by senders - `send` message. This command must be used with sender's ID, if recipient's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)). +#### Secure queue by sender + +This command is used from v8 of SMP protocol. V8 and earlier uses [KEY](#secure-queue-by-recipient). + +This command is sent by the sender to the server to add sender's key to the queue: + +```abnf +sndSecure = %s"SKEY " senderAuthPublicKey +senderAuthPublicKey = length x509encoded +; the sender's Ed25519 or X25519 key to verify SEND commands for this queue +``` + +Once the queue is secured only authorized messages can be sent to it. + +This command MUST be used in transmission with sender queue ID. + #### Send message This command is sent to the server by the sender both to confirm the queue after the sender received out-of-band message from the recipient and to send messages after the queue is secured: @@ -605,64 +726,79 @@ This command is sent to the server by the sender both to confirm the queue after send = %s"SEND " msgFlags SP smpEncMessage msgFlags = notificationFlag reserved notificationFlag = %s"T" / %s"F" -smpEncMessage = smpPubHeader sentMsgBody ; message up to 16088 bytes -smpPubHeader = smpClientVersion ("1" senderPublicDhKey / "0") -smpClientVersion = word16 +smpEncMessage = smpEncClientMessage / smpEncConfirmation ; message up to 16064 bytes + +smpEncClientMessage = smpPubHeaderNoKey msgNonce sentClientMsgBody ; message up to 16064 bytes +smpPubHeaderNoKey = smpClientVersion "0" +sentClientMsgBody = 16016*16016 OCTET + +smpEncConfirmation = smpPubHeaderWithKey msgNonce sentConfirmationBody +smpPubHeaderWithKey = smpClientVersion "1" senderPublicDhKey + ; sender's Curve25519 public key to agree DH secret for E2E encryption in this queue + ; it is only sent in confirmation message +sentConfirmationBody = 15920*15920 OCTET ; E2E-encrypted smpClientMessage padded to 16016 bytes before encryption senderPublicDhKey = length x509encoded -; sender's Curve25519 public key to agree DH secret for E2E encryption in this queue -; it is only sent in confirmation message + +smpClientVersion = word16 x509encoded = -sentMsgBody = 16032*16032 OCTET -; E2E-encrypted smpClientMessage padded to 16032 bytes before encryption +msgNonce = 24*24 OCTET word16 = 2*2 OCTET ``` -The first message is sent to confirm the queue - it should contain sender's server key (see decrypted message syntax below) - this first message must be sent without signature. +The first message is sent to confirm the queue - it should contain sender's server key (see decrypted message syntax below) - this first message may be sent without authorization. -Once the queue is secured (see [Secure queue command](#secure-queue-command)), the following send commands must be sent with the signature. +Once the queue is secured (see [Secure queue by sender](#secure-queue-by-sender)), the subsequent `SEND` commands must be sent with the authorization. The server must respond with `"ERR AUTH"` response in the following cases: - the queue does not exist or is suspended -- the queue is secured but the transmission does NOT have a signature -- the queue is NOT secured but the transmission has a signature +- the queue is secured but the transmission does NOT have a authorization +- the queue is NOT secured but the transmission has a authorization -Until the queue is secured, the server should accept any number of unsigned messages - it both enables the legitimate sender to resend the confirmation in case of failure and also allows the simplex messaging client to ignore any confirmation messages that may be sent by the attackers (assuming they could have intercepted the queue ID in the server response, but do not have a correct encryption key passed to sender in out-of-band message). +The server must respond with `"ERR QUOTA"` response when queue capacity is exceeded. The number of messages that the server can hold is defined by the server configuration. When sender reaches queue capacity the server will not accept any further messages until the recipient receives ALL messages from the queue. After the last message is delivered, the server will deliver an additional special message indicating that the queue capacity was reached. See [Deliver queue message](#deliver-queue-message) -The body should be encrypted with the recipient's "public" key (`EK`); once decrypted it must have this format: +Until the queue is secured, the server should accept any number of unsigned messages (up to queue capacity) - it allows the sender to resend the confirmation in case of failure. + +The body should be encrypted with the shared secret based on recipient's "public" key (`EK`); once decrypted it must have this format: ```abnf -sentMsgBody = -smpClientMessage = smpPrivHeader clientMsgBody -smpPrivHeader = emptyHeader / smpConfirmationHeader -emptyHeader = " " -smpConfirmationHeader = %s"K" senderKey +sentClientMsgBody = +smpClientMessage = emptyHeader clientMsgBody +emptyHeader = "_" +clientMsgBody = *OCTET ; up to 16016 - 2 + +sentConfirmationBody = +smpConfirmation = smpConfirmationHeader confirmationBody +smpConfirmationHeader = emptyHeader / %s"K" senderKey + ; emptyHeader is used when queue is already secured by sender +confirmationBody = *OCTET ; up to 15920 - 2 senderKey = length x509encoded -; the sender's Ed25519 or Ed448 public key to sign SEND commands for this queue -clientMsgBody = *OCTET ; up to 16016 in case of emptyHeader + ; the sender's Ed25519 or X25519 public key to authorize SEND commands for this queue ``` `clientHeader` in the initial unsigned message is used to transmit sender's server key and can be used in the future revisions of SMP protocol for other purposes. -SMP transmission structure for sent messages: +SMP transmission structure for directly sent messages: ``` -------- transmission (= 16384 bytes) +------- transmissions (= 16384 bytes) + 1 | transmission count (= 1) 2 | originalLength - 276- | signature sessionId corrId queueId %s"SEND" SP (1+114 + 1+32? + 1+32 + 1+24 + 4+1 = 210) - ....... smpEncMessage (= 16088 bytes = 16384 - 296 bytes) + 299- | authorization sessionId corrId queueId %s"SEND" SP (1+114 + 1+32? + 1+24 + 1+24 + 4+1 = 203) + ....... smpEncMessage (= 16064 bytes = 16384 - 320 bytes) 8- | smpPubHeader (for messages it is only version and '0' to mean "no DH key" = 3 bytes) 24 | nonce for smpClientMessage 16 | auth tag for smpClientMessage - ------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 48) + ------- smpClientMessage (E2E encrypted, = 16016 bytes = 16064 - 48) 2 | originalLength - 12- | smpPrivHeader + 2- | smpPrivHeader ....... - | clientMsgBody (<= 16016 bytes = 16032 - 14) + | clientMsgBody (<= 16012 bytes = 16016 - 4) ....... 0+ | smpClientMessage pad ------- smpClientMessage end | + 0+ | message pad ....... smpEncMessage end 18+ | transmission pad ------- transmission end @@ -671,20 +807,23 @@ SMP transmission structure for sent messages: SMP transmission structure for received messages: ``` -------- transmission (= 16384 bytes) +------- transmissions (= 16384 bytes) + 1 | transmission count (= 1) 2 | originalLength - 276- | signature sessionId corrId queueId %s"MSG" SP msgId timestamp (1+114 + 1+32? + 1+32 + 1+24 + 3+1 + 24+1 + 8 = 243) + 283- | authorization sessionId corrId queueId %s"MSG" SP msgId (1+114 + 1+32? + 1+24 + 1+24 + 3+1 + 1+24 = 227) 16 | auth tag (msgId is used as nonce) - ------- serverEncryptedMsg (= 16090 bytes = 16384 - 294 bytes) + ------- serverEncryptedMsg (= 16082 bytes = 16384 - 302 bytes) 2 | originalLength - ....... smpEncMessage (= 16088 bytes = 16090 - 2 bytes) - 16- | smpPubHeader (empty header for the message) + 8 | timestamp + 8- | message flags + ....... smpEncMessage (= 16064 bytes = 16082 - 18 bytes) + 8- | smpPubHeader (empty header for the message) 24 | nonce for smpClientMessage 16 | auth tag for smpClientMessage - ------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 56 bytes) + ------- smpClientMessage (E2E encrypted, = 16016 bytes = 16064 - 48 bytes) 2 | originalLength - 16- | smpPrivHeader (empty header for the message) - ....... clientMsgBody (<= 16016 bytes = 16032 - 16) + 2- | smpPrivHeader (empty header for the message) + ....... clientMsgBody (<= 16012 bytes = 16016 - 4) -- TODO move internal structure (below) to agent protocol 20- | agentPublicHeader (the size is for user messages post handshake, without E2E X3DH keys - it is version and 'M' for the messages - 3 bytes in total) ....... E2E double-ratchet encrypted (<= 15996 bytes = 16016 - 20) @@ -719,6 +858,149 @@ SMP transmission structure for received messages: ------- transmission end ``` +### Proxying sender commands + +To protect transport (IP address and session) anonymity of the sender from the server chosen (and, potentially, controlled) by the recipient SMP v8 added support for proxying sender's command to the recipient's server via the server chosen by the sender. + +Sequence diagram for sending the message and `SKEY` commands via SMP proxy: + +``` +------------- ------------- ------------- ------------- +| sending | | SMP | | SMP | | receiving | +| client | | proxy | | server | | client | +------------- ------------- ------------- ------------- + | `PRXY` | | | + | -------------------------> | | | + | | ------------------------------> | | + | | SMP handshake | | + | | <------------------------------ | | + | `PKEY` | | | + | <------------------------- | | | + | | | | + | `PFWD` (s2r) | | | + | -------------------------> | | | + | | `RFWD` (p2r) | | + | | ------------------------------> | | + | | `RRES` (p2r) | | + | | <------------------------------ | | + | `PRES` (s2r) | | `MSG` | + | <------------------------- | | -----------------------> | + | | | `ACK` | + | | | <----------------------- | + | | | | + | | | | +``` + +1. The client requests (`PRXY` command) the chosen server to connect to the destination SMP server and receives (`PKEY` response) the session information, including server certificate and the session key signed by this certificate. To protect client session anonymity the proxy MUST re-use the same session with all clients that request connection with any given destination server. + +2. The client encrypts the transmission (`SKEY` or `SEND`) to the destination server using the shared secret computed from per-command random key and server's session key and sends it to proxying server in `PFWD` command. + +3. Proxy additionally encrypts the body to prevent correlation by ciphertext (in case TLS is compromised) and forwards it to proxy in `RFWD` command. + +4. Proxy receives the double-encrypted response from the destination server, removes one encryption layer and forwards it to the client. + +The diagram below shows the encryption layers for `PFWD`/`RFWD` commands and `RRES`/`PRES` responses: + +- s2r - encryption between client and SMP relay, with relay key returned in relay handshake, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. This encryption prevents proxy server from observing commands and responses - proxy does not know how many different queues a connected client sends messages and commands to. +- e2e - end-to-end encryption per SMP queue, with additional client encryption inside it. +- p2r - additional encryption between proxy and SMP relay with the shared secret agreed in the handshake, to mitigate traffic correlation inside TLS. +- r2c - additional encryption between SMP relay and client to prevent traffic correlation inside TLS. + +``` +----------------- ----------------- -- TLS -- ----------------- ----------------- +| | -- TLS -- | | -- p2r -- | | -- TLS -- | | +| | -- s2r -- | | -- s2r -- | | -- r2c -- | | +| sending | -- e2e -- | | -- e2e -- | | -- e2e -- | receiving | +| client | MSG | SMP proxy | MSG | SMP server | MSG | client | +| | -- e2e -- | | -- e2e -- | | -- e2e -- | | +| | -- s2r -- | | -- s2r -- | | -- r2c -- | | +| | -- TLS -- | | -- p2r -- | | -- TLS -- | | +----------------- ----------------- -- TLS -- ----------------- ----------------- +``` + +SMP proxy is not another type of the server, it is a role that any SMP server can play when forwarding the commands. + +#### Request proxied session + +The sender uses this command to request the session with the destination proxy. + +Servers SHOULD support basic auth with this command, to allow only server owners and trusted users to proxy commands to the destination servers. + +```abnf +proxySession = %s"PRXY" SP smpServer basicAuth +smpServer = hosts port fingerprint +hosts = length 1*host +host = shortString +port = shortString +fingerprint = shortString +basicAuth = "0" / "1" shortString ; server password +``` + +```abnf +proxySessionKey = %s"PKEY" SP sessionId smpVersionRange certChain signedKey +sessionId = shortString + ; Session ID (tlsunique) of the proxy with the destination server. + ; This session ID should be used as entity ID in transmission with `PFWD` command +certChain = length 1*cert +cert = originalLength x509encoded +signedKey = originalLength x509encoded ; key signed with certificate +originalLength = 2*2 OCTET +``` + +When the client receives PKEY response it MUST validate that: +- the fingerprint of the received certificate matches fingerprint in the server address - it mitigates MITM attack by proxy. +- the server session key is correctly signed with the received certificate. + +The proxy server may respond with error response in case the destination server is not available or in case it has an earlier version that does not support proxied commands. + +#### Send command via proxy + +Sender can send `SKEY` and `SEND` commands via proxy after obtaining the session ID with `PRXY` command (see [Request proxied session](#request-proxied-session)). + +Transmission sent to proxy server should use session ID as entity ID and use a random correlation ID of 24 bytes as a nonce for crypto_box encryption of transmission to the destination server. The random ephemeral X25519 key to encrypt transmission should be unique per command, and it should be combined with the key sent by the server in the handshake header to proxy and to the client in `PKEY` command. + +Encrypted transmission should use the received session ID from the connection between proxy server and destination server in the authorized body. + +```abnf +proxyCommand = %s"PFWD" SP smpVersion commandKey +smpVersion = 2*2 OCTET +commandKey = length x509encoded +``` + +The proxy server will forward the encrypted transmission in `RFWD` command (see below). + +Having received the `RRES` response from the destination server, proxy server will forward `PRES` response to the client. `PRES` response should use the same correlation ID as `PFWD` command. The destination server will use this correlation ID increased by 1 as a nonce for encryption of the response. + +```abnf +proxyResponse = %s"PRES" SP +``` + +#### Forward command to destination server + +Having received `PFWD` command from the client, the server should additionally encrypt it (without padding, as the received transmission is already encrypted by the client and padded to a fixed size) together with the correlation ID, sender command key, and protocol version, and forward it to the destination server as `RFWD` command: + +Transmission forwarded to relay uses empty entity ID and its unique random correlation ID is used as a nonce to encrypt forwarded transmission. Correlation ID increased by 1 is used by the destination server as a nonce to encrypt responses. + +```abnf +relayCommand = %s"RFWD" SP +forwardedTransmission = fwdCorrId fwdSmpVersion fwdCommandKey transmission +fwdCorrId = length 24*24 OCTET + ; `fwdCorrId` - correlation ID used in `PFWD` command transmission - it is used as a nonce for client encryption, + ; and `fwdCorrId + 1` is used as a nonce for the destination server response encryption. +fwdSmpVersion = 2*2 OCTET +fwdCommandKey = length x509encoded +transmission = *OCTET ; note that it is not prefixed with the length +``` + +The destination server having received this command decrypts both encryption layers (proxy and client), verifies client authorization as usual, processes it, and send the double encrypted `RRES` response to proxy. + +The shared secret for encrypting transmission bodies between proxy server and destination server is agreed from proxy and destination server keys exchanged in handshake headers - proxy and server use the same shared secret during the session for the encryption between them. + + +```abnf +relayResponse = %s"RRES" SP +``` + ### Notifier commands #### Subscribe to queue notifications @@ -735,45 +1017,40 @@ The first message notification will be delivered either immediately or as soon a ### Server messages -#### Queue IDs response +This section includes server events and generic command responses used for several commands. -Server must respond with this message when the new queue is created. - -See its syntax in [Create queue command](#create-queue-command) +The syntax for command-specific responses is shown together with the commands. #### Deliver queue message -The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is: - -```abnf -message = %s"MSG " msgId encryptedRcvMsgBody -encryptedMsgBody = ; server-encrypted padded sent msgBody -paddedSentMsgBody = ; maxMessageLength = 16088 -encryptedRcvMsgBody = ; server-encrypted meta-data and padded sent msgBody -rcvMsgBody = timestamp msgFlags SP paddedSentMsgBody -msgId = length 24*24OCTET -timestamp = 8*8OCTET -``` - -`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement). Message ID is used as a nonce for server/recipient encryption of message bodies. - -`timestamp` - system time when the server received the message from the sender as **a number of seconds** since Unix epoch (1970-01-01) encoded as 64-bit integer in network byte order. If a client system/language does not support 64-bit integers, until 2106 it is safe to simply skip the first 4 zero bytes and decode 32-bit unsigned integer (or as signed integer until 2038). - -`paddedSentMsgBody` - see syntax in [Send message](#send-message) - When server delivers the messages to the recipient, message body should be encrypted with the secret derived from DH exchange using the keys passed during the queue creation and returned with `queueIds` response. This is done to prevent the possibility of correlation of incoming and outgoing traffic of SMP server inside transport protocol. -#### Notifier queue ID response +The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is: -Server must respond with this message when queue notifications are enabled. +```abnf +message = %s"MSG" SP msgId encryptedRcvMsgBody +encryptedRcvMsgBody = + ; server-encrypted padded sent msgBody + ; maxMessageLength = 16064 +rcvMsgBody = timestamp msgFlags SP sentMsgBody / msgQuotaExceeded +msgQuotaExceeded = %s"QUOTA" SP timestamp +msgId = length 24*24OCTET +timestamp = 8*8OCTET +``` -See its syntax in [Enable notifications command](#enable-notifications-command) +If the sender exceeded queue capacity the recipient will receive a special message indicating the quota was exceeded. This can be used in the higher level protocol to notify sender client that it can continue sending messages. + +`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement). Message ID is used as a nonce for server/recipient encryption of message bodies. + +`timestamp` - system time when the server received the message from the sender as **a number of seconds** since Unix epoch (1970-01-01) encoded as 64-bit integer in network byte order. If a client system/language does not support 64-bit integers, until 2106 it is safe to simply skip the first 4 zero bytes and decode 32-bit unsigned integer (or as signed integer until 2038). + +`sentMsgBody` - message sent by `SEND` command. See [Send message](#send-message). #### Deliver message notification -The server must deliver message notifications to all simplex queues that were subscribed with `subscribeNotifications` command ("NSUB") on the currently open transport connection. The syntax for the message notification delivery is: +The server must deliver message notifications to all simplex queues that were subscribed with `subscribeNotifications` command (`NSUB`) on the currently open transport connection. The syntax for the message notification delivery is: ```abnf messageNotification = %s"NMSG " nmsgNonce encryptedNMsgMeta @@ -802,28 +1079,60 @@ No further messages should be delivered to unsubscribed transport connection. #### Error responses -- incorrect block format, encoding or signature size (`BLOCK`). +- incorrect block format, encoding or authorization size (`BLOCK`). - missing or different session ID - tls-unique binding of TLS transport (`SESSION`) - command errors (`CMD`): - - error parsing command (`SYNTAX`) - - prohibited command (`PROHIBITED`) - any server response sent from client or `ACK` sent without active subscription or without message delivery. - - transmission has no required signature or queue ID (`NO_AUTH`) + - unknown command (`UNKNOWN`). + - error parsing command (`SYNTAX`). + - prohibited command (`PROHIBITED`): + - `ACK` sent without active subscription or without message delivery. + - `GET` and `SUB` used in the same transport connection with the same queue. + - transmission has no required authorization or queue ID (`NO_AUTH`) - transmission has unexpected credentials (`HAS_AUTH`) - - transmission has no required queue ID (`NO_QUEUE`) -- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message](#send-message) command). + - transmission has no required queue ID (`NO_ENTITY`) +- proxy server errors (`PROXY`): + - `PROTOCOL` - any error. + - `BASIC_AUTH` - incorrect basic auth. + - `NO_SESSION` - no destination server session with passed ID. + - `BROKER` - destination server error: + - `RESPONSE` - invalid server response (failed to parse). + - `UNEXPECTED` - unexpected response. + - `NETWORK` - network error. + - `TIMEOUT` - command response timeout. + - `HOST` - no compatible server host (e.g. onion when public is required, or vice versa) + - `TRANSPORT` - handshake or other transport error: + - `BLOCK` - error parsing transport block. + - `VERSION` - incompatible client or server version. + - `LARGE_MSG` - message too large. + - `SESSION` - incorrect session ID. + - `NO_AUTH` - absent server key - when the server did not provide a DH key to authorize commands for the queue that should be authorized with a DH key. + - `HANDSHAKE` - transport handshake error: + - `PARSE` - handshake syntax (parsing) error. + - `IDENTITY` - incorrect server identity (certificate fingerprint does not match server address). + - `BAD_AUTH` - incorrect or missing server credentials in handshake. +- authentication error (`AUTH`) - incorrect authorization, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message](#send-message) command). - message queue quota exceeded error (`QUOTA`) - too many messages were sent to the message queue. Further messages can only be sent after the recipient retrieves the messages. -- sent message is too large (> 16088) to be delivered (`LARGE_MSG`). +- sent message is too large (> 16064) to be delivered (`LARGE_MSG`). - internal server error (`INTERNAL`). The syntax for error responses: ```abnf error = %s"ERR " errorType -errorType = %s"BLOCK" / %s"SESSION" / %s"CMD " cmdError / %s"AUTH" / %s"LARGE_MSG" /%s"INTERNAL" +errorType = %s"BLOCK" / %s"SESSION" / %s"CMD" SP cmdError / %s"PROXY" proxyError / + %s"AUTH" / %s"QUOTA" / %s"LARGE_MSG" / %s"INTERNAL" cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_ENTITY" +proxyError = %s"PROTOCOL" SP errorType / %s"BROKER" SP brokerError / + %s"BASIC_AUTH" / %s"NO_SESSION" +brokerError = %s"RESPONSE" SP shortString / %s"UNEXPECTED" SP shortString / + %s"NETWORK" / %s"TIMEOUT" / %s"HOST" / + %s"TRANSPORT" SP transportError +transportError = %s"BLOCK" / %s"VERSION" / %s"LARGE_MSG" / %s"SESSION" / %s"NO_AUTH" / + %s"HANDSHAKE" SP handshakeError +handshakeError = %s"PARSE" / %s"IDENTITY" / %s"BAD_AUTH" ``` -Server implementations must aim to respond within the same time for each command in all cases when `"ERR AUTH"` response is required to prevent timing attacks (e.g., the server should perform signature verification even when the queue does not exist on the server or the signature of different size is sent, using any RSA key with the same size as the signature size). +Server implementations must aim to respond within the same time for each command in all cases when `"ERR AUTH"` response is required to prevent timing attacks (e.g., the server should verify authorization even when the queue does not exist on the server or the authorization of different type is sent, using any dummy key compatible with the used authorization). ### OK response @@ -833,22 +1142,12 @@ When the command is successfully executed by the server, it should respond with ok = %s"OK" ``` -## Appendices +## Transport connection with the SMP server -### Appendix A. - -**SMP transport protocol.** +### General transport protocol considerations Both the recipient and the sender can use TCP or some other, possibly higher level, transport protocol to communicate with the server. The default TCP port for SMP server is 5223. -For scenarios when meta-data privacy is critical, it is recommended that clients: - -- communicating over Tor network, -- establish a separate connection for each SMP queue, -- send noise traffic (using PING command). - -In addition to that, the servers can be deployed as Tor onion services. - The transport protocol should provide the following: - server authentication (by matching server certificate hash with `serverIdentity`), @@ -856,40 +1155,110 @@ The transport protocol should provide the following: - integrity (preventing data modification by the attacker without detection), - unique channel binding (`sessionIdentifier`) to include in the signed part of SMP transmissions. -By default, the client and server communicate using [TLS 1.3 protocol][13] restricted to: +### TLS transport encryption + +The client and server communicate using [TLS 1.3 protocol][13] restricted to: - TLS_CHACHA20_POLY1305_SHA256 cipher suite (for better performance on mobile devices), -- ed25519 and ed448 EdDSA algorithms for signatures, -- x25519 and x448 ECDHE groups for key exchange. -- servers must send the chain of exactly 2 self-signed certificates in the handshake, with the first (offline) certificate one signing the second (online) certificate. Offline certificate fingerprint is used as a server identity - it is a part of SMP server address. +- ed25519 EdDSA algorithms for signatures, +- x25519 ECDHE groups for key exchange. +- servers must send the chain of 2, 3 or 4 self-signed certificates in the handshake (see [Server certificate](#server-certificate)), with the first (offline) certificate one signing the second (online) certificate. Offline certificate fingerprint is used as a server identity - it is a part of SMP server address. - The clients must abort the connection in case a different number of certificates is sent. - server and client TLS configuration should not allow resuming the sessions. During TLS handshake the client must validate that the fingerprint of the online server certificate is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection. +### Server certificate + +Servers use self-signed certificates that the clients validate by comparing the fingerprint of one of the certificates in the chain with the certificate fingerprint present in the server address. + +Clients SHOULD support the chains of 2, 3 and 4 server certificates: + +**2 certificates**: +1. offline server certificate: + - its fingerprint is present in the server address. + - its private key is not stored on the server. +2. online server certificate: + - it must be signed by offline certificate. + - its private key is stored on the server and is used in TLS session. + +**3 certificates**: +1. offline server certificate - same as with 2 certificates. +2. online server certificate: + - it must be signed by offline certificate. + - its private key is stored on the server. +3. session certificate: + - generated automatically on every server start and/or on schedule. + - signed by online server certificate. + - its private key is used in TLS session. + +**4 certificates**: +0. offline operator identity certificate: + - used for all servers operated by the same entity. + - its private key is not stored on the server. +1. offline server certificate: + - signed by offline operator certificate. + - same as with 2 certificates. +2. online server certificate - same as with 3 certificates. +3. session certificate - same as with 3 certificates. + +### ALPN to agree handshake version + +Client and server use [ALPN extension][18] of TLS to agree handshake version. + +Server SHOULD send `smp/1` protocol name and the client should confirm this name in order to use the current protocol version. This is added to allow support of older clients without breaking backward compatibility and to extend or modify handshake syntax. + +If the client does not confirm this protocol name, the server would fall back to v6 of SMP protocol. + +### Transport handshake + Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes). -The first block sent by the server should be `serverHello` and the client should respond with `clientHello` - these blocks are used to agree SMP protocol version: +The first block sent by the server should be `paddedServerHello` and the client should respond with `paddedClientHello` - these blocks are used to agree SMP protocol version: ```abnf -serverHello = minSmpVersion maxSmpVersion sessionIdentifier pad +paddedServerHello = +serverHello = smpVersionRange sessionIdentifier [serverCert signedServerKey] ignoredPart +smpVersionRange = minSmpVersion maxSmpVersion minSmpVersion = smpVersion maxSmpVersion = smpVersion -sessionIdentifier = length *OCTET +sessionIdentifier = shortString ; unique session identifier derived from transport connection handshake -; it should be included in all SMP transmissions sent in this transport connection. +; it should be included in authorized part of all SMP transmissions sent in this transport connection, +; but it must not be sent as part of the transmission in the current protocol version. +serverCert = originalLength x509encoded +signedServerKey = originalLength x509encoded ; signed by server certificate -clientHello = smpVersion pad +paddedClientHello = +clientHello = smpVersion [clientKey] ignoredPart ; chosen SMP protocol version - it must be the maximum supported version ; within the range offered by the server +clientKey = length x509encoded smpVersion = 2*2OCTET ; Word16 version number - +originalLength = 2*2OCTET +ignoredPart = *OCTET pad = *OCTET ``` +`signedServerKey` is used to compute a shared secret to authorize client transmission - it is combined with the per-queue key that was used when the queue was created. + +`clientKey` is used only by SMP proxy server when it connects to the destination server to agree shared secret for the additional encryption layer, end user clients do not use this key. + +`ignoredPart` in handshake allows to add additional parameters in handshake without changing protocol version - the client and servers must ignore any extra bytes within the original block length. + For TLS transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol (possibly, with another channel binding). +### Additional transport privacy + +For scenarios when meta-data privacy is critical, it is recommended that clients: + +- communicating over Tor network, +- establish a separate connection for each SMP queue, +- send noise traffic (using PING command). + +In addition to that, the servers can be deployed as Tor onion services. + [1]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack [2]: https://en.wikipedia.org/wiki/End-to-end_encryption [3]: https://en.wikipedia.org/wiki/QR_code @@ -906,3 +1275,5 @@ For TLS transport client should assert that `sessionIdentifier` is equal to `tls [14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3 [15]: https://www.rfc-editor.org/rfc/rfc8709.html [16]: https://nacl.cr.yp.to/box.html +[17]: https://datatracker.ietf.org/doc/html/rfc8927 +[18]: https://datatracker.ietf.org/doc/html/rfc7301 diff --git a/protocol/xftp.md b/protocol/xftp.md new file mode 100644 index 000000000..d6d271837 --- /dev/null +++ b/protocol/xftp.md @@ -0,0 +1,632 @@ +Version 2, 2024-06-22 + +# SimpleX File Transfer Protocol + +## Table of contents + +- [Abstract](#abstract) +- [Introduction](#introduction) +- [XFTP Model](#xftp-model) +- [Persistence model](#persistence-model) +- [XFTP procedure](#xftp-procedure) +- [File description](#file-description) +- [URIs syntax](#uris-syntax) + - [XFTP server URI](#xftp-server-uri) + - [File description URI](#file-description-URI) +- [XFTP qualities and features](#xftp-qualities-and-features) +- [Cryptographic algorithms](#cryptographic-algorithms) +- [File chunk IDs](#file-chunk-ids) +- [Server security requirements](#server-security-requirements) +- [Transport protocol](#transport-protocol) + - [TLS ALPN](#tls-alpn) + - [Connection handshake](#connection-handshake) + - [Requests and responses](#requests-and-responses) +- [XFTP commands](#xftp-commands) + - [Correlating responses with commands](#correlating-responses-with-commands) + - [Command authentication](#command-authentication) + - [Keep-alive command](#keep-alive-command) + - [File sender commands](#file-sender-commands) + - [Register new file chunk](#register-new-file-chunk) + - [Add file chunk recipients](#add-file-chunk-recipients) + - [Upload file chunk](#upload-file-chunk) + - [Delete file chunk](#delete-file-chunk) + - [File recipient commands](#file-recipient-commands) + - [Download file chunk](#download-file-chunk) + - [Acknowledge file chunk download](#acknowledge-file-chunk-download) +- [Threat model](#threat-model) + +## Abstract + +SimpleX File Transfer Protocol is a client-server protocol for asynchronous unidirectional file transmission. + +It's designed with the focus on communication security, integrity and meta-data privacy, under the assumption that any part of the message transmission network can be compromised. + +It is designed as a application level protocol to solve the problem of secure and private file transmission, making [MITM attacks][1] very difficult at any part of the file transmission system, and preserving meta-data privacy of the sent files. + +## Introduction + +The objective of SimpleX File Transfer Protocol (XFTP) is to facilitate the secure and private unidirectional transfer of files from senders to recipients via persistent file chunks stored by the xftp server. + +XFTP is implemented as an application level protocol on top of HTTP2 and TLS. + +The protocol describes the set of commands that senders and recipients can send to XFTP servers to create, upload, download and delete file chunks of several pre-defined sizes. XFTP servers SHOULD support chunks of 4 sizes: 64KB, 256KB, 1MB and 4MB (1KB = 1024 bytes, 1MB = 1024KB). + +The protocol is designed with the focus on meta-data privacy and security. While using TLS, the protocol does not rely on TLS security by using additional encryption to achieve that there are no identifiers or ciphertext in common in received and sent server traffic, frustrating traffic correlation even if TLS is compromised. + +XFTP does not use any form of participants' identities. It relies on out-of-band passing of "file description" - a human-readable YAML document with the list of file chunk locations, hashes and necessary cryptographic keys. + +## XFTP Model + +The XFTP model has three communication participants: the recipient, the file server (XFTP server) that is chosen and, possibly, controlled by the sender, and the sender. + +XFTP server allows uploading fixed size file chunks, with or without basic authentication. The same party that can be the sender of one file chunk can be the recipient of another, without exposing it to the server. + +Each file chunk allows multiple recipients, each recipient can download the same chunk multiple times. It allows depending on the threat model use the same recipient credentials for multiple parties, thus reducing server ability to understand the number of intended recipients (but server can still track IP addresses to determine it), or use one unique set of credentials for each recipient, frustrating traffic correlation on the assumption of compromised TLS. In the latter case, senders can create a larger number of recipient credentials to hide the actual number of intended recipients from the servers (which is what SimpleX clients do). + +``` + Sender Internet XFTP relays Internet Recipient +---------------------------- | ----------------- | ------------------- | ------------ | ---------- + | | | | + | | (can be self-hosted) | | + | | +---------+ | | + chunk 1 ----- HTTP2 over TLS ------ | XFTP | ---- HTTP2 / TLS ----- chunk 1 + |---> SimpleX File Transfer Protocol (XFTP) --> | Relay | ---> XFTP ------------->| + | --------------------------- +---------+ ---------------------- | + | | | | | | + | | | | | v + +----------+ | | +---------+ | | +-------------+ + | Sending | ch. 2 ------- HTTP2 / TLS ------- | XFTP | ---- HTTP2 / TLS ---- ch. 2 | Receiving | +file ---> | XFTP | ------> XFTP ----> | Relay | ---> XFTP ------> | XFTP | ---> file + | Client | --------------------------- +---------+ ---------------------- | Client | + +----------+ | | | | +-------------+ + | | | | | ^ + | | | +---------+ | | | + | ------- HTTP2 / TLS ------- | XFTP | ---- HTTP2 / TLS ---- | + |-------------> XFTP ----> | Relay | ---> XFTP ------------->| + chunk N --------------------------- +---------+ --------------------- chunk N + | | (store file chunks) | | + | | | | + | | | | +``` + +When sender client uploads a file chunk, it has to register it first with one sender ID and multiple recipient IDs, and one random unique key per ID to authenticate sender and recipients, and also provide its size and hash that will be validated when chunk is uploaded. + +To send the actual file, the sender client MUST pad it and encrypt it with a random symmetric key and distribute chunks of fixed sized across multiple XFTP servers. Information about chunk locations, keys, hashes and required keys is passed to the recipients as "[file description](#file-description)" out-of-band. + +Creating, uploading, downloading and deleting file chunks requires sending commands to the XFTP server - they are described in detail in [XFTP commands](#xftp-commands) section. + +## Persistence model + +Server stores file chunk records in memory, with optional adding to append-only log, to allow restoring them on server restart. File chunk bodies can be stored as files or as objects in any object store (e.g. S3). + +## XFTP procedure + +1. Sending the file. + +To send the file, the sender will: + +1) Prepare file + - compute its SHA512 digest. + - prepend header with the name and pad the file to match the whole number of chunks in size. It is RECOMMENDED to use 2 of 4 allowed chunk sizes, to balance upload size and metadata privacy. + - encrypt it with a randomly chosen symmetric key and IV (e.g., using NaCL secret_box). + - split into allowed size chunks. + - generate per-recipient keys. It is recommended that the sending client generates more per-recipient keys than the actual number of recipients, rounding up to a power of 2, to conceal the actual number of intended recipients. + +2) Upload file chunks + - register each chunk record with randomly chosen one or more (for redundancy) XFTP server(s). + - optionally request additional recipient IDs, if required number of recipient keys didn't fit into register request. + - upload each chunk to chosen server(s). + +3) Prepare file descriptions, one per recipient. + +The sending client combines addresses of all chunks and other information into "file description", different for each file recipient, that will include: + +- an encryption key used to encrypt/decrypt the full file (the same for all recipients). +- file SHA512 digest to validate download. +- list of chunk descriptions; information for each chunk: + - private Ed25519 key to sign commands for file transfer server. + - chunk address (server host and chunk ID). + - chunk sha512 digest. + +To reduce the size of file description, chunks are grouped by the server host. + +4) Send file description(s) to the recipient(s) out-of-band, via pre-existing secure and authenticated channel. E.g., SimpleX clients send it as messages via SMP protocol, but it can be done via any other channel. + +![Sending file](./diagrams/xftp/xftp-sending-file.svg) + +2. Receiving the file. + +Having received the description, the recipient will: + +1) Download all chunks. + +The receiving client can fall back to secondary servers, if necessary: +- if the server is not available. +- if the chunk is not present on the server (ERR AUTH response). +- if the hash of the downloaded file chunk does not match the description. + +Optionally recipient can acknowledge file chunk reception to delete file ID from server for this recipient. + +2) Combine the chunks into a file. + +3) Decrypt the file using the key in file description. + +4) Extract file name and unpad the file. + +5) Validate file digest with the file description. + +![Receiving file](./diagrams/xftp/xftp-receiving-file.svg) + +## File description + +"File description" is a human-readable YAML document that is sent via secure and authenticated channel. + +It includes these fields: +- `party` - "sender" or "recipient". Sender's file description is required to delete the file. +- `size` - padded file size equal to total size of all chunks, see `fileSize` syntax below. +- `digest` - SHA512 hash of encrypted file, base64url encoded string. +- `key` - symmetric encryption key to decrypt the file, base64url encoded string. +- `nonce` - nonce to decrypt the file, base64url encoded string. +- `chunkSize` - default chunk size, see `fileSize` syntax below. +- `replicas` - the array of file chunk replicas descriptions. +- `redirect` - optional property for redirect information indicating that the file is itself a description to another file, allowing to use file description as a short URI. + +Each replica description is an object with 2 fields: + +- `chunks` - and array of chunk replica descriptions stored on one server. +- `server` - [server address](#xftp-server-uri) where the chunks can be downloaded from. + +Each server replica description is a string with this syntax: + +```abnf +chunkReplica = chunkNo ":" replicaId ":" replicaKey [":" chunkDigest [":" chunkSize]] +chunkNo = 1*DIGIT + ; a sequential 1-based chunk number in the original file. +replicaId = base64url + ; server-assigned random chunk replica ID. +replicaKey = base64url + ; sender-generated random key to receive (or to delete, in case of sender's file description) the chunk replica. +chunkDigest = base64url + ; chunk digest that MUST be specified for the first replica of each chunk, + ; and SHOULD be omitted (or be the same) on the subsequent replicas +chunkSize = fileSize +fileSize = sizeInBytes / sizeInUnits + ; chunk size SHOULD only be specified on the first replica and only if it is different from default chunk size +sizeInBytes = 1*DIGIT +sizeInUnits = 1*DIGIT sizeUnit +sizeUnit = %s"kb" / %s"mb" / %s"gb" +base64url = ; RFC4648, section 5 +``` + +Optional redirect information has two fields: +- `size` - the size of the original encrypted file to which file description downloaded via the current file description will lead to, see `fileSize` syntax below. +- `digest` - SHA512 hash of the original file, base64url encoded string. + +## URIs syntax + +### XFTP server URI + +The XFTP server address is a URI with the following syntax: + +```abnf +xftpServerURI = %s"xftp://" xftpServer +xftpServer = serverIdentity [":" basicAuth] "@" srvHost [":" port] +srvHost = ; RFC1123, RFC5891 +port = 1*DIGIT +serverIdentity = base64url +basicAuth = base64url +``` + +### File description URI + +This file description URI can be generated by the client application to share a small file description as a QR code or as a link. Practically, to be able to scan a QR code it should be under 1000 characters, so only file descriptions with 1-2 chunks can be used in this case. This is supported with `redirect` property when file description leads to a file which in itself is a larger file description to another file - akin to URL shortener. + +File description URI syntax: + +```abnf +fileDescriptionURI = serviceScheme "/file" "#/?desc=" description [ "&data=" userData ] +serviceScheme = (%s"https://" clientAppServer) | %s"simplex:" +clientAppServer = hostname [ ":" port ] +; client app server, e.g. simplex.chat +description = +userData = +``` + +clientAppServer is not a server the client connects to - it is a server that shows the instruction on how to download the client app that will connect using this connection request. This server can also host a mobile or desktop app manifest so that this link is opened directly in the app if it is installed on the device. + +"simplex" URI scheme in serviceScheme can be used instead of client app server. Client apps MUST support this URI scheme. + +## XFTP qualities and features + +XFTP stands for SimpleX File Transfer Protocol. Its design is based on the same ideas and has some of the qualities of SimpleX Messaging Protocol: + +- recipient cannot see sender's IP address, as the file fragments (chunks) are temporarily stored on multiple XFTP relays. +- file can be sent asynchronously, without requiring the sender to be online for file to be received. +- there is no network of peers that can observe this transfer - sender chooses which XFTP relays to use, and can self-host their own. +- XFTP relays do not have any file metadata - they only see individual chunks, with access to each chunk authorized with anonymous credentials (using Edwards curve cryptographic signature) that are random per chunk. +- chunks have one of the sizes allowed by the servers - 64KB, 256KB, 1MB and 4MB chunks, so sending a large file looks indistinguishable from sending many small files to XFTP server. If the same transport connection is reused, server would only know that chunks are sent by the same user. +- each chunk can be downloaded by multiple recipients, but each recipient uses their own key and chunk ID to authorize access, and the chunk is encrypted by a different key agreed via ephemeral DH keys (NaCl crypto_box (SalsaX20Poly1305 authenticated encryption scheme ) with shared secret derived from Curve25519 key exchange) on the way from the server to each recipient. XFTP protocol as a result has the same quality as SMP protocol - there are no identifiers and ciphertext in common between sent and received traffic inside TLS connection, so even if TLS is compromised, it complicates traffic correlation attacks. +- XFTP protocol supports redundancy - each file chunk can be sent via multiple relays, and the recipient can choose the one that is available. Current implementation of XFTP protocol in SimpleX Chat does not support redundancy though. +- the file as a whole is encrypted with a random symmetric key using NaCl secret_box. + +## Cryptographic algorithms + +Clients must cryptographically authorize XFTP commands, see [Command authentication](#command-authentication). + +To authorize/verify transmissions clients and servers MUST use either signature algorithm Ed25519 algorithm defined in RFC8709 or using deniable authentication scheme based on NaCL crypto_box (see Simplex Messaging Protocol). + +To encrypt/decrypt file chunk bodies delivered to the recipients, servers/clients MUST use NaCL crypto_box. + +Clients MUST encrypt file chunk bodies sent via XFTP servers using use NaCL crypto_box. + +## File chunk IDs + +XFTP servers MUST generate a separate new set of IDs for each new chunk - for the sender (that uploads the chunk) and for each intended recipient. It is REQUIRED that: + +- These IDs are different and unique within the server. +- Based on random bytes generated with cryptographically strong pseudo-random number generator. + +## Server security requirements + +XFTP server implementations MUST NOT create, store or send to any other servers: + +- Logs of the client commands and transport connections in the production environment. + +- History of retrieved files. + +- Snapshots of the database they use to store file chunks (instead clients can manage redundancy by creating chunk replicas using more than one XFTP server). In-memory persistence is recommended for file chunks records. + +- Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using XFTP servers. + +## Transport protocol + +- binary-encoded commands sent as fixed-size padded block in the body of HTTP2 POST request, similar to SMP and notifications server protocol transmission encodings. +- HTTP2 POST with a fixed size padded block body for file upload and download. + +Block size - 4096 bytes (it would fit ~120 Ed25519 recipient keys). + +The reasons to use HTTP2: + +- avoid the need to have two hostnames (or two different ports) for commands and file uploads. +- compatibility with the existing HTTP2 client libraries. + +The reason not to use JSON bodies: + +- bigger request size, so fewer recipient keys would fit in a single request +- signature over command has to be outside of JSON anyway. + +The reason not to use URI segments / HTTP verbs / REST semantics is to have consistent request size. + +### ALPN to agree handshake version + +Client and server use [ALPN extension][18] of TLS to agree handshake version. + +Server SHOULD send `xftp/1` protocol name and the client should confirm this name in order to use the current protocol version. This is added to allow support of older clients without breaking backward compatibility and to extend or modify handshake syntax. + +If the client does not confirm this protocol name, the server would fall back to v1 of XFTP protocol. + +### Transport handshake + +When a client and a server agree on handshake version using ALPN extension, they should proceed with XFTP handshake. + +As with SMP, a client doesn't reveal its version range to avoid version fingerprinting. Unlike SMP, XFTP runs a HTTP2 protocol over TLS and the server can't just send its handshake right away. So a session handshake is driven by client-sent requests: + +1. To pass initiative to the server, the client sends a request with empty body. +2. Server responds with its `paddedServerHello` block. +3. Clients sends a request containing `paddedClientHello` block, +4. Server sends an empty response, finalizing the handshake. + +Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes). + +```abnf +paddedServerHello = +serverHello = xftpVersionRange sessionIdentifier serverCert signedServerKey ignoredPart +xftpVersionRange = minXftpVersion maxXftpVersion +minXftpVersion = xftpVersion +maxXftpVersion = xftpVersion +sessionIdentifier = shortString +; unique session identifier derived from transport connection handshake +serverCert = originalLength +signedServerKey = originalLength ; signed by server certificate + +paddedClientHello = +clientHello = xftpVersion keyHash ignoredPart +; chosen XFTP protocol version - must be the maximum supported version +; within the range offered by the server + +xftpVersion = 2*2OCTET ; Word16 version number +keyHash = shortString +shortString = length length*OCTET +length = 1*1OCTET +originalLength = 2*2OCTET +ignoredPart = *OCTET +``` + +In XFTP v2 the handshake is only used for version negotiation, but `serverCert` and `signedServerKey` must be validated by the client. + +`keyHash` is the CA fingerprint used by client to validate TLS certificate chain and is checked by a server against its own key. + +`ignoredPart` in handshake allows to add additional parameters in handshake without changing protocol version - the client and servers must ignore any extra bytes within the original block length. + +For TLS transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol (possibly, with another channel binding). + +### Requests and responses + +- File sender: + - create file chunk record. + - Parameters: + - Ed25519 key for subsequent sender commands and Ed25519 keys for commands of each recipient. + - chunk size. + - Response: + - chunk ID for the sender and different IDs for all recipients. + - add recipients to file chunk + - Parameters: + - sender's chunk ID + - Ed25519 keys for commands of each recipient. + - Response: + - chunk IDs for new recipients. + - upload file chunk. + - delete file chunk (invalidates all recipient IDs). +- File recipient: + - download file chunk: + - chunk ID + - DH key for additional encryption of the chunk. + - command should be signed with the key passed by the sender when creating chunk record. + - delete file chunk ID (only for one recipient): signed with the same key. + +## XFTP commands + +Commands syntax below is provided using ABNF with case-sensitive strings extension. + +```abnf +xftpCommand = ping / senderCommand / recipientCmd / serverMsg +senderCommand = register / add / put / delete +recipientCmd = get / ack +serverMsg = pong / sndIds / rcvIds / ok / file +``` + +The syntax of specific commands and responses is defined below. + +### Correlating responses with commands + +Commands are made via HTTP2 requests, responses to commands are correlated as HTTP2 responses. + +### Command authentication + +XFTP servers must authenticate all transmissions (excluding `ping`) by verifying the client signatures. Command signature should be generated by applying the algorithm specified for the file to the `signed` block of the transmission, using the key associated with the file chunk ID (recipient's or sender's depending on which file chunk ID is used). + +### Keep-alive command + +To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `pong` response. This command should be sent unsigned and without file chunk ID. + +```abnf +ping = %s"PING" +``` + +This command is always sent unsigned. + + data FileResponse = ... | FRPong | ... + +```abnf +pong = %s"PONG" +``` + +### File sender commands + +Sending any of the commands in this section (other than `register`, that is sent without file chunk ID) is only allowed with sender's ID. + +#### Register new file chunk + +This command is sent by the sender to the XFTP server to register a new file chunk. + +Servers SHOULD support basic auth with this command, to allow only server owners and trusted users to create file chunks on the servers. + +The syntax is: + +```abnf +register = %s"FNEW " fileInfo rcvPublicAuthKeys basicAuth +fileInfo = sndKey size digest +sndKey = length x509encoded +size = 1*DIGIT +digest = length *OCTET +rcvPublicAuthKeys = length 1*rcvPublicAuthKey +rcvPublicAuthKey = length x509encoded +basicAuth = "0" / "1" length *OCTET + +x509encoded = + +length = 1*1 OCTET +``` + +If the file chunk is registered successfully, the server must send `sndIds` response with the sender's and recipients' file chunk IDs: + +```abnf +sndIds = %s"SIDS " senderId recipientIds +senderId = length *OCTET +recipientIds = length 1*recipientId +recipientId = length *OCTET +``` + +#### Add file chunk recipients + +This command is sent by the sender to the XFTP server to add additional recipient keys to the file chunk record, in case number of keys requested by client didn't fit into `register` command. The syntax is: + +```abnf +add = %s"FADD " rcvPublicAuthKeys +rcvPublicAuthKeys = length 1*rcvPublicAuthKey +rcvPublicAuthKey = length x509encoded +``` + +If additional keys were added successfully, the server must send `rcvIds` response with the added recipients' file chunk IDs: + +```abnf +rcvIds = %s"RIDS " recipientIds +recipientIds = length 1*recipientId +recipientId = length *OCTET +``` + +#### Upload file chunk + +This command is sent by the sender to the XFTP server to upload file chunk body to server. The syntax is: + +```abnf +put = %s"FPUT" +``` + +Chunk body is streamed via HTTP2 request. + +If file chunk body was successfully received, the server must send `ok` response. + +```abnf +ok = %s"OK" +``` + +#### Delete file chunk + +This command is sent by the sender to the XFTP server to delete file chunk from the server. The syntax is: + +```abnf +delete = %s"FDEL" +``` + +Server should delete file chunk record, invalidating all recipient IDs, and delete file body from file storage. If file chunk was successfully deleted, the server must send `ok` response. + +### File recipient commands + +Sending any of the commands in this section is only allowed with recipient's ID. + +#### Download file chunk + +This command is sent by the recipient to the XFTP server to download file chunk body from the server. The syntax is: + +```abnf +get = %s"FGET " rDhKey +rDhKey = length x509encoded +``` + +If requested file is successfully located, the server must send `file` response. File chunk body is sent as HTTP2 response body. + +```abnf +file = %s"FILE " sDhKey cbNonce +sDhKey = length x509encoded +cbNonce = +``` + +Chunk is additionally encrypted on the way from the server to the recipient using a key agreed via ephemeral DH keys `rDhKey` and `sDhKey`, so there is no ciphertext in common between sent and received traffic inside TLS connection, in order to complicate traffic correlation attacks, if TLS is compromised. + +#### Acknowledge file chunk download + +This command is sent by the recipient to the XFTP server to acknowledge file reception, deleting file ID from server for this recipient. The syntax is: + +```abnf +ack = %s"FACK" +``` + +If file recipient ID is successfully deleted, the server must send `ok` response. + +In current implementation of XFTP protocol in SimpleX Chat clients don't use FACK command. Files are automatically expired on servers after configured time interval. + +## Threat model + +#### Global Assumptions + + - A user protects their local database and key material. + - The user's application is authentic, and no local malware is running. + - The cryptographic primitives in use are not broken. + - A user's choice of servers is not directly tied to their identity or otherwise represents distinguishing information about the user. + +#### A passive adversary able to monitor the traffic of one user + +*can:* + + - identify that and when a user is sending files over XFTP protocol. + + - determine which servers the user sends/receives files to/from. + + - observe how much traffic is being sent, and make guesses as to its purpose. + +*cannot:* + + - see who sends files to the user and who the user sends the files to. + +#### A passive adversary able to monitor a set of file senders and recipients + + *can:* + + - learn which XFTP servers are used to send and receive files for which users. + + - learn when files are sent and received. + + - perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers. + + - observe how much traffic is being sent, and make guesses as to its purpose + +*cannot, even in case of a compromised transport protocol:* + + - perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol + +#### XFTP server + +*can:* + +- learn when file senders and recipients are online. + +- know how many file chunks and chunk sizes are sent via the server. + +- perform the correlation of the file chunks as belonging to one file via either a re-used transport connection, user's IP address, or connection timing regularities. + +- learn file senders' and recipients' IP addresses, and infer information (e.g. employer) based on the IP addresses, as long as Tor is not used. + +- delete file chunks, preventing file delivery, as long as redundant delivery is not used. + +- lie about the state of a file chunk to the recipient and/or to the sender (e.g. deleted when it is not). + +- refuse deleting the file when instructed by the sender. + +*cannot:* + +- undetectably corrupt file chunks. + +- learn the contents, name or the exact size of sent files. + +- learn approximate size of sent files, as long as more than one server is used to send file chunks. + +- compromise the users' end-to-end encryption of files with an active attack. + +#### An attacker who obtained Alice's (decrypted) chat database + +*can:* + +- see the history of all files exchanged by Alice with her communication partners, as long as files were not deleted from the database. + +- receive all files sent and received by Alice that did not expire yet, as long as information about these files was not removed from the database. + +- prevent Alice's contacts from receiving the files she sent by deleting all or some of the file chunks from XFTP servers. + +#### A user's contact + +*can:* + +- spam the user with files. + +- forever retain files from the user. + +*cannot:* + +- cryptographically prove to a third-party that a file came from a user (assuming the user's device is not seized). + +- prove that two contacts they have is the same user. + +- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user, even if they receive the same file. + +#### An attacker with Internet access + +*can:* + +- Denial of Service XFTP servers. + +*cannot:* + +- send files to a user who they are not connected with. + +- enumerate file chunks on an XFTP server. diff --git a/protocol/xrcp.md b/protocol/xrcp.md new file mode 100644 index 000000000..9f7187e66 --- /dev/null +++ b/protocol/xrcp.md @@ -0,0 +1,330 @@ +Version 1, 2024-06-22 + +# SimpleX Remote Control Protocol + +## Table of contents + +- [Abstract](#abstract) +- [XRCP model](#xrcp-model) +- [Transport protocol](#transport-protocol) + - [Session invitation](#session-invitation) + - [Establishing TLS connection](#establishing-tls-connection) + - [Session verification and protocol negotiation](#session-verification-and-protocol-negotiation) + - [Controller/host session operation](#сontrollerhost-session-operation) +- [Key agreement for announcement packet and for session](#key-agreement-for-announcement-packet-and-for-session) +- [Threat model](#threat-model) + +## Abstract + +The SimpleX Remote Control Protocol is a client-server protocol designed to transform application UIs into thin clients, enabling remote control from another device. This approach allows users to remotely access and utilize chat profiles without the complexities of master-master replication for end-to-end encryption states. + +Like SMP and XFTP, XRCP leverages out-of-band invitations to mitigate MITM attacks and employs multiple cryptographic layers to safeguard application data. + +## XRCP model + +XRCP assumes two application roles: host (that contain the application data) and controller that gains limited access to host data. +Applications are also split into two components: UI and core. + +When an XRCP session is established a host UI is locked out and a controller UI uses its core to proxy commands to the host core, getting back responses and events. + +``` + + +------+ +------+ xrcp +------+ +------+ + | Ctrl | commands | Ctrl | commands | Host | | Host | +user ---> | UI | -----------> | Core | -----------> | Core | | UI | + +------+ +------+ +------+ +------+ + ^ responses | ^ xrcp responses | ^ + |<------------------| |<-----------------| | +-------------+ + | events | | | Application |-+ + |<------------------| |----> | protocol | | + | servers | | + +-------------+ | + +--------------+ +``` + +## Transport protocol + +Protocol consists of four phases: +- controller session invitation +- establishing session TLS connection +- session verification and protocol negotiation +- session operation + +![Session sequence](./diagrams/xrcp/session.svg) + +### Session invitation + +The invitation to the first session between host and controller pair MUST be shared out-of-band, to establish a long term identity keys/certificates of the controller to host device. + +The subsequent sessions can be announced via an application-defined site-local multicast group, e.g. `224.0.0.251` (also used in mDNS/bonjour) and an application-defined port (SimpleX Chat uses 5227). + +The session invitation contains this data: +- supported version range for remote control protocol. +- application-specific information, e.g. device name, application name and supported version range, settings, etc. +- session start time in seconds since epoch. +- if multicast is used, counter of announce packets sent by controller. +- network address (ipv4 address and port) of the controller. +- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements. +- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first. +- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity. +- Session X25519 DH key and SNTRUP761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption. + +Host application decrypts (except the first session) and validates the invitation: +- Session signature is valid. +- Timestamp is within some window from the current time. +- Long-term key signature is valid. +- Long-term CA and signature key are the same as in the first session. +- Some version in the offered range is supported. + +OOB session invitation is a URI with this syntax: + +```abnf +sessionAddressUri = "xrcp:/" encodedCAFingerprint "@" host ":" port "#/?" qsParams +encodedCAFingerprint = base64url +host = ; in textual form, RFC4001 +port = 1*DIGIT ; uint16 +qsParams = param *("&" param) +param = versionRangeParam / appInfoParam / sessionTsParam / + sessPubKeyParam / idPubKeyParam / dhPubKeyParam / + sessSignatureParam / idSignatureParam +versionRangeParam = "v=" (versionParam / (versionParam "-" versionParam)) +versionParam = 1*DIGIT +appInfoParam = "app=" escapedJSON +sessionTsParam = "ts=" 1*DIGIT +sessPubKeyParam = "skey=" base64url +idPubKeyParam = "idkey=" base64url +dhPubKeyParam = "dh=" base64url +sessSignatureParam = "ssig=" base64url ; signs the URI with this and idSignatureParam param removed +idSignatureParam = "idsig=" base64url ; signs the URI with this param removed +base64url = ; RFC4648, section 5 +``` + +Multicast session announcement is a binary encoded packet with this syntax: + +```abnf +sessionAddressPacket = dhPubKey nonce encrypted(unpaddedSize sessionAddress packetPad) +dhPubKey = length x509encoded ; same as announced +nonce = length *OCTET +sessionAddress = largeLength sessionAddressUri ; as above +length = 1*1 OCTET ; for binary data up to 255 bytes +largeLength = 2*2 OCTET ; for binary data up to 65535 bytes +packetPad = ; possibly, we may need to move KEM agreement one step later, +; with encapsulation key in HELLO block and KEM ciphertext in reply to HELLO. +``` + +### Establishing TLS connection + +Both controller and host use 2-element certificate chains with unique self-signed CA root representing long-term identities. Leaf certificates aren't stored and instead generated on each session start. + +A controller runs a TCP server to avoid opening listening socket on a host, which might create an attack vector. A controller keeps no sensitive data to be exposed this way. + +During TLS handshake, parties validate certificate chains against previously known (from invitation or storage) CA fingerprints. The fingerprints MUST be the same as in the invitation and in the subsequent connections. + +### Session verification and protocol negotiation + +Once TLS session is established, both the host and controller devices present a "session security code" to the user who must match them (e.g., visually or via QR code scan) and confirm on the host device. The session security code must be a digest of tlsunique channel binding. As it is computed as a digest of the TLS handshake for both the controller and the host, it will validate that the same TLS certificates are used on both sides, and that the same TLS session is established, mitigating the possibility of MITM attack in the connection. + +Once the session is confirmed by the user, the host sends HELLO block to the controller. + +XRCP blocks inside TLS are padded to 16384 bytes. + +Host HELLO block must contain: +- new session DH key - used to compute new shared secret with the controller keys from the announcement. +- encrypted part of HELLO block (JSON object), containing: + - chosen protocol version. + - host CA TLS certificate fingerprint - part of host long term identity - must match the one presented in TLS handshake and the previous sessions, otherwise the connection is terminated. + - KEM encapsulation key - used to compute new shared secret for the session. + - additional application specific parameters, e.g host device name, application version, host settings or JSON encoding format. + +Host HELLO block syntax: + +```abnf +hostHello = %s"HELLO " dhPubKey nonce encrypted(unpaddedSize hostHelloJSON helloPad) pad +unpaddedSize = largeLength +dhPubKey = length x509encoded +pad = +helloPad = +largeLength = 2*2 OCTET +``` + +The controller decrypts (including the first session) and validates the received HELLO block: +- Chosen versions are supported (must be within offered ranges). +- CA fingerprint matches the one presented in TLS handshake and the previous sessions - in subsequent sessions TLS connection should be rejected if the fingerprint is different. + +[JTD schema](https://www.rfc-editor.org/rfc/rfc8927) for the encrypted part of host HELLO block `hostHelloJSON`: + +```json +{ + "definitions": { + "version": { + "type": "string", + "metadata": { + "format": "[0-9]+" + } + }, + "base64url": { + "type": "string", + "metadata": { + "format": "base64url" + } + } + }, + "properties": { + "v": {"ref": "version"}, + "ca": {"ref": "base64url"}, + "kem": {"ref": "base64url"} + }, + "optionalProperties": { + "app": {"properties": {}, "additionalProperties": true} + }, + "additionalProperties": true +} +``` + +The controller should reply with with `ctrlHello` or `ctrlError` response: + +```abnf +ctrlHello = %s"HELLO " kemCiphertext nonce encrypted(unpaddedSize ctrlHelloJSON helloPad) pad +; ctrlHelloJSON is encrypted with the hybrid secret, +; including both previously agreed DH secret and KEM secret from kemCiphertext +unpaddedSize = largeLength +kemCiphertext = largeLength *OCTET +pad = +helloPad = +largeLength = 2*2 OCTET + +ctrlError = %s"ERROR " nonce encrypted(unpaddedSize ctrlErrorMessage helloPad) pad +ctrlErrorMessage = ; encrypted using previously agreed DH secret. +``` + +JTD schema for the encrypted part of controller HELLO block `ctrlHelloJSON`: + +```json +{ + "properties": {}, + "additionalProperties": true +} +``` + +Once the controller replies HELLO to the valid host HELLO block, it should stop accepting new TCP connections. + +### Controller/host session operation + +The protocol for communication during the session is out of scope of this protocol. + +SimpleX Chat uses HTTP2 encoding, where host device acts as a server and controller acts as a client (these roles are reversed compared with TLS connection, restoring client-server semantics in HTTP). + +Payloads in the protocol must be encrypted using NaCL secret_box using the hybrid shared secret agreed during session establishment. + +Commands of the controller must be signed after the encryption using the controller's session and long term Ed25519 keys. + +tlsunique channel binding from TLS session MUST be included in commands (included in the signed body). + +The syntax for encrypted command and response body encoding: + +```abnf +commandBody = encBody sessSignature idSignature [attachment] +responseBody = encBody [attachment] ; counter must match command +encBody = nonce encLength32 encrypted(tlsunique counter body) +attachment = %x01 nonce encLength32 encrypted(attachment) +noAttachment = %x00 +tlsunique = length 1*OCTET +counter = 8*8 OCTET ; int64 +encLength32 = 4*4 OCTET ; uint32, includes authTag +``` + +If the command or response includes attachment, its hash must be included in command/response and validated. + +## Key agreement for announcement packet and for session + +Initial announcement is shared out-of-band (URI with xrcp scheme), and it is not encrypted. + +This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA256 over concatenated DH shared secret and KEM encapsulated secret) both for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses. + +During the next session the announcement is sent via encrypted multicast block. The shared key for this announcement and for host HELLO block is determined using the KEM shared secret from the previous session and DH shared secret computed using the host DH key from the previous session and the new controller DH key from the announcement. + +For the session, the shared secret is computed again using the KEM shared secret encapsulated by the controller using the new KEM key from the host HELLO block and DH shared secret computed using the host DH key from HELLO block and the new controller DH key from the announcement. + +In pseudo-code: + +``` +// session 1 +hostHelloSecret(1) = dhSecret(1) +sessionSecret(1) = sha256(dhSecret(1) || kemSecret(1)) // to encrypt session 1 data, incl. controller hello +dhSecret(1) = dh(hostHelloDhKey(1), controllerInvitationDhKey(1)) +kemCiphertext(1) = enc(kemSecret(1), kemEncKey(1)) +// kemEncKey is included in host HELLO, kemCiphertext - in controller HELLO +kemSecret(1) = dec(kemCiphertext(1), kemDecKey(1)) + +// multicast announcement for session n +announcementSecret(n) = sha256(dhSecret(n')) +dhSecret(n') = dh(hostHelloDhKey(n - 1), controllerDhKey(n)) + +// session n +hostHelloSecret(n) = dhSecret(n) +sessionSecret(n) = sha256(dhSecret(n) || kemSecret(n)) // to encrypt session n data, incl. controller hello +dhSecret(n) = dh(hostHelloDhKey(n), controllerDhKey(n)) +// controllerDhKey(n) is either from invitation or from multicast announcement +kemCiphertext(n) = enc(kemSecret(n), kemEncKey(n)) +kemSecret(n) = dec(kemCiphertext(n), kemDecKey(n)) +``` + +If controller fails to store the new host DH key after receiving HELLO block, the encryption will become out of sync and the host won't be able to decrypt the next announcement. To mitigate it, the host should keep the last session DH key and also previous session DH key to try to decrypt the next announcement computing shared secret using both keys (first the new one, and in case it fails - the previous). + +To decrypt a multicast announcement, the host should try to decrypt it using the keys of all known (paired) remote controllers. + +## Threat model + +#### A passive network adversary able to monitor the site-local traffic: + +*can:* +- observe session times, duration and volume of the transmitted data between host and controller. + +*cannot:* +- observe the content of the transmitted data. +- substitute the transmitted commands or responses. +- replay transmitted commands or events from the hosts. + +#### An active network adversary able to intercept and substitute the site-local traffic: + +*can:* +- prevent host and controller devices from establishing the session + +*cannot:* +- same as passive adversary, provided that user visually verified session code out-of-band. + +#### An active adversary with the access to the network: + +*can:* +- spam controller device. + +*cannot:* +- compromise host or controller devices. + +#### An active adversary with the access to the network who also observed OOB announcement: + +*can:* +- connect to controller instead of the host. +- present incorrect data to the controller. + +*cannot:* +- connect to the host or make host connect to itself. + +#### Compromised controller device: + +*can:* +- observe the content of the transmitted data. +- access any data of the controlled host application, within the capabilities of the provided API. + +*cannot:* +- access other data on the host device. +- compromise host device. + +#### Compromised host device: + +*can:* +- present incorrect data to the controller. +- incorrectly interpret controller commands. + +*cannot:* +- access controller data, even related to this host device. diff --git a/rfcs/2022-12-26-simplex-file-transfer.md b/rfcs/done/2022-12-26-simplex-file-transfer.md similarity index 100% rename from rfcs/2022-12-26-simplex-file-transfer.md rename to rfcs/done/2022-12-26-simplex-file-transfer.md diff --git a/rfcs/2022-12-27-queue-quota.md b/rfcs/done/2022-12-27-queue-quota.md similarity index 100% rename from rfcs/2022-12-27-queue-quota.md rename to rfcs/done/2022-12-27-queue-quota.md diff --git a/rfcs/2023-05-02-resync-ratchets.md b/rfcs/done/2023-05-02-resync-ratchets.md similarity index 100% rename from rfcs/2023-05-02-resync-ratchets.md rename to rfcs/done/2023-05-02-resync-ratchets.md diff --git a/rfcs/2023-05-03-delivery-receipts.md b/rfcs/done/2023-05-03-delivery-receipts.md similarity index 100% rename from rfcs/2023-05-03-delivery-receipts.md rename to rfcs/done/2023-05-03-delivery-receipts.md diff --git a/rfcs/2023-05-24-smp-delivery-proxy.md b/rfcs/done/2023-05-24-smp-delivery-proxy.md similarity index 100% rename from rfcs/2023-05-24-smp-delivery-proxy.md rename to rfcs/done/2023-05-24-smp-delivery-proxy.md diff --git a/rfcs/2023-06-08-resync-ratchets.md b/rfcs/done/2023-06-08-resync-ratchets.md similarity index 100% rename from rfcs/2023-06-08-resync-ratchets.md rename to rfcs/done/2023-06-08-resync-ratchets.md diff --git a/rfcs/2023-09-12-second-relays.md b/rfcs/done/2023-09-12-second-relays.md similarity index 100% rename from rfcs/2023-09-12-second-relays.md rename to rfcs/done/2023-09-12-second-relays.md diff --git a/rfcs/2023-10-25-remote-control.md b/rfcs/done/2023-10-25-remote-control.md similarity index 100% rename from rfcs/2023-10-25-remote-control.md rename to rfcs/done/2023-10-25-remote-control.md diff --git a/rfcs/2024-01-26-file-links.md b/rfcs/done/2024-01-26-file-links.md similarity index 100% rename from rfcs/2024-01-26-file-links.md rename to rfcs/done/2024-01-26-file-links.md diff --git a/rfcs/2024-02-03-deniability.md b/rfcs/done/2024-02-03-deniability.md similarity index 100% rename from rfcs/2024-02-03-deniability.md rename to rfcs/done/2024-02-03-deniability.md diff --git a/rfcs/2024-03-28-xftp-version.md b/rfcs/done/2024-03-28-xftp-version.md similarity index 100% rename from rfcs/2024-03-28-xftp-version.md rename to rfcs/done/2024-03-28-xftp-version.md diff --git a/rfcs/2021-02-28-streams.md b/rfcs/rejected/2021-02-28-streams.md similarity index 100% rename from rfcs/2021-02-28-streams.md rename to rfcs/rejected/2021-02-28-streams.md diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 0067d4ada..d6bbc13ca 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -332,7 +332,6 @@ data AEvent (e :: AEntity) where UP :: SMPServer -> [ConnId] -> AEvent AENone SWITCH :: QueueDirection -> SwitchPhase -> ConnectionStats -> AEvent AEConn RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> AEvent AEConn - MID :: AgentMsgId -> PQEncryption -> AEvent AEConn SENT :: AgentMsgId -> Maybe SMPServer -> AEvent AEConn MWARN :: AgentMsgId -> AgentErrorType -> AEvent AEConn MERR :: AgentMsgId -> AgentErrorType -> AEvent AEConn @@ -401,7 +400,6 @@ data AEventTag (e :: AEntity) where UP_ :: AEventTag AENone SWITCH_ :: AEventTag AEConn RSYNC_ :: AEventTag AEConn - MID_ :: AEventTag AEConn SENT_ :: AEventTag AEConn MWARN_ :: AEventTag AEConn MERR_ :: AEventTag AEConn @@ -454,7 +452,6 @@ aEventTag = \case UP {} -> UP_ SWITCH {} -> SWITCH_ RSYNC {} -> RSYNC_ - MID {} -> MID_ SENT {} -> SENT_ MWARN {} -> MWARN_ MERR {} -> MERR_ From 9e7e0d102dc39846103c71101b84d793f16ab8ab Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:15:08 +0300 Subject: [PATCH 101/125] smp-server: conserve resources (#1194) * transport: force auth params, remove async wrapper * stricter new messages * bang more thunks * style * don't produce msgQuota unless requested * strict * refactor * remove bangs --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- src/Simplex/Messaging/Agent.hs | 4 +++- src/Simplex/Messaging/Client.hs | 17 +++++++++-------- src/Simplex/Messaging/Client/Agent.hs | 6 +++--- src/Simplex/Messaging/Notifications/Server.hs | 2 +- src/Simplex/Messaging/Server.hs | 13 +++++++------ src/Simplex/Messaging/Server/MsgStore/STM.hs | 5 +++-- src/Simplex/Messaging/Server/QueueStore/STM.hs | 3 ++- src/Simplex/Messaging/Transport.hs | 10 +++++++--- 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 63f74b5b2..f1ab78200 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -427,7 +427,9 @@ setNetworkConfig c@AgentClient {useNetworkConfig} cfg' = do (_, cfg) <- readTVar useNetworkConfig if cfg == cfg' then pure False - else True <$ (writeTVar useNetworkConfig $! (slowNetworkConfig cfg', cfg')) + else + let cfgSlow = slowNetworkConfig cfg' + in True <$ (cfgSlow `seq` writeTVar useNetworkConfig (cfgSlow, cfg')) when changed $ reconnectAllServers c setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO () diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index e4413d595..de178e368 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -100,6 +100,7 @@ module Simplex.Messaging.Client where import Control.Applicative ((<|>)) +import Control.Concurrent (ThreadId, forkFinally, killThread, mkWeakThreadId) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception @@ -138,13 +139,14 @@ import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Transport.WebSockets (WS) import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM) import Simplex.Messaging.Version +import System.Mem.Weak (Weak, deRefWeak) import System.Timeout (timeout) -- | 'SMPClient' is a handle used to send commands to a specific SMP server. -- -- Use 'getSMPClient' to connect to an SMP server and create a client handle. data ProtocolClient v err msg = ProtocolClient - { action :: Maybe (Async ()), + { action :: Maybe (Weak ThreadId), thParams :: THandleParams v 'TClient, sessionTs :: UTCTime, client_ :: PClient v err msg @@ -475,15 +477,14 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize cVar <- newEmptyTMVarIO let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN} username = proxyUsername transportSession - action <- - async $ - runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar) - `finally` atomically (tryPutTMVar cVar $ Left PCENetworkError) + tId <- + runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar) + `forkFinally` \_ -> void (atomically . tryPutTMVar cVar $ Left PCENetworkError) c_ <- tcpConnectTimeout `timeout` atomically (takeTMVar cVar) case c_ of - Just (Right c') -> pure $ Right c' {action = Just action} + Just (Right c') -> mkWeakThreadId tId >>= \tId' -> pure $ Right c' {action = Just tId'} Just (Left e) -> pure $ Left e - Nothing -> cancel action $> Left PCENetworkError + Nothing -> killThread tId $> Left PCENetworkError useTransport :: (ServiceName, ATransport) useTransport = case port srv of @@ -589,7 +590,7 @@ proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" ( -- | Disconnects client from the server and terminates client threads. closeProtocolClient :: ProtocolClient v err msg -> IO () -closeProtocolClient = mapM_ uninterruptibleCancel . action +closeProtocolClient = mapM_ (deRefWeak >=> mapM_ killThread) . action {-# INLINE closeProtocolClient #-} -- | SMP client error type. diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 8781d87a6..e7c22eec2 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -1,8 +1,7 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} @@ -171,7 +170,8 @@ getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worke case r of Right smp -> do logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv - let c = (isOwnServer ca srv, smp) + let !owned = isOwnServer ca srv + !c = (owned, smp) atomically $ do putTMVar (sessionVar v) (Right c) TM.insert (sessionId $ thParams smp) c smpSessions diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 892560660..5d3b4d806 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -382,7 +382,7 @@ send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO () send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do t <- atomically $ readTBQueue sndQ void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)] - atomically . writeTVar sndActiveAt =<< liftIO getSystemTime + atomically . (writeTVar sndActiveAt $!) =<< liftIO getSystemTime -- instance Show a => Show (TVar a) where -- show x = unsafePerformIO $ show <$> readTVarIO x diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index dfb4973ea..5c8d13a5e 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -525,7 +525,7 @@ receive h@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiv labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive" forever $ do ts <- L.toList <$> liftIO (tGet h) - atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime + atomically . (writeTVar rcvActiveAt $!) =<< liftIO getSystemTime stats <- asks serverStats (errs, cmds) <- partitionEithers <$> mapM (cmdAction stats) ts write sndQ errs @@ -581,7 +581,7 @@ tSend :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> NonEmpty (Tran tSend th Client {sndActiveAt} ts = do withMVar th $ \h@THandle {params} -> void . tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts - atomically . writeTVar sndActiveAt =<< liftIO getSystemTime + atomically . (writeTVar sndActiveAt $!) =<< liftIO getSystemTime disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do @@ -1037,15 +1037,16 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi mkMessage body = do msgId <- randomId =<< asks (msgIdBytes . config) msgTs <- liftIO getSystemTime - pure $ Message msgId msgTs msgFlags body + pure $! Message msgId msgTs msgFlags body expireMessages :: MsgQueue -> M () expireMessages q = do msgExp <- asks $ messageExpiration . config old <- liftIO $ mapM expireBeforeEpoch msgExp - stats <- asks serverStats deleted <- atomically $ sum <$> mapM (deleteExpiredMsgs q) old - atomically $ modifyTVar' (msgExpired stats) (+ deleted) + when (deleted > 0) $ do + stats <- asks serverStats + atomically $ modifyTVar' (msgExpired stats) (+ deleted) trySendNotification :: NtfCreds -> Message -> TVar ChaChaDRG -> STM (Maybe Bool) trySendNotification NtfCreds {notifierId, rcvNtfDhSecret} msg ntfNonceDrg = @@ -1164,7 +1165,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi msgTs' = messageTs msg setDelivered :: Sub -> Message -> STM Bool - setDelivered s msg = tryPutTMVar (delivered s) (messageId msg) + setDelivered s msg = tryPutTMVar (delivered s) $! messageId msg getStoreMsgQueue :: T.Text -> RecipientId -> M MsgQueue getStoreMsgQueue name rId = time (name <> " getMsgQueue") $ do diff --git a/src/Simplex/Messaging/Server/MsgStore/STM.hs b/src/Simplex/Messaging/Server/MsgStore/STM.hs index 2d735d1d4..e315c4fe5 100644 --- a/src/Simplex/Messaging/Server/MsgStore/STM.hs +++ b/src/Simplex/Messaging/Server/MsgStore/STM.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -75,7 +76,7 @@ snapshotMsgQueue st rId = TM.lookup rId st >>= maybe (pure []) (snapshotTQueue . pure msgs writeMsg :: MsgQueue -> Message -> STM (Maybe Message) -writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} msg = do +writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = do canWrt <- readTVar canWrite empty <- isEmptyTQueue q if canWrt || empty @@ -85,7 +86,7 @@ writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} msg = do modifyTVar' size (+ 1) if canWrt' then writeTQueue q msg $> Just msg - else writeTQueue q msgQuota $> Nothing + else (writeTQueue q $! msgQuota) $> Nothing else pure Nothing where msgQuota = MessageQuota {msgId = msgId msg, msgTs = msgTs msg} diff --git a/src/Simplex/Messaging/Server/QueueStore/STM.hs b/src/Simplex/Messaging/Server/QueueStore/STM.hs index 8de7a38c6..d6cdaf10a 100644 --- a/src/Simplex/Messaging/Server/QueueStore/STM.hs +++ b/src/Simplex/Messaging/Server/QueueStore/STM.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} @@ -69,7 +70,7 @@ secureQueue QueueStore {queues} rId sKey = readTVar qVar >>= \q -> case senderKey q of Just k -> pure $ if sKey == k then Just q else Nothing _ -> - let q' = q {senderKey = Just sKey} + let !q' = q {senderKey = Just sKey} in writeTVar qVar q' $> Just q' addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> STM (Either ErrorType QueueRec) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6eddcabf8..7088480f5 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -83,7 +83,7 @@ module Simplex.Messaging.Transport where import Control.Applicative (optional) -import Control.Monad (forM) +import Control.Monad (forM, (<$!>)) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) import qualified Data.Aeson.TH as J @@ -540,12 +540,12 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer smpTHandleServer th v vr pk k_ = - let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_} + let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$!> k_} in smpTHandle_ th v vr (Just thAuth) smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient smpTHandleClient th v vr pk_ ck_ = - let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_ + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = forceCertChain ck, sessSecret = C.dh' k <$!> pk_}) <$!> ck_ in smpTHandle_ th v vr thAuth smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p @@ -554,6 +554,10 @@ smpTHandle_ th@THandle {params} v vr thAuth = let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion} in (th :: THandleSMP c p) {params = params'} +{-# INLINE forceCertChain #-} +forceCertChain :: (X.CertificateChain, X.SignedExact T.PubKey) -> (X.CertificateChain, X.SignedExact T.PubKey) +forceCertChain cert@(X.CertificateChain cc, signedKey) = length (show cc) `seq` show signedKey `seq` cert + -- This function is only used with v >= 8, so currently it's a simple record update. -- It may require some parameters update in the future, to be consistent with smpTHandle_. smpTHParamsSetVersion :: VersionSMP -> THandleParams SMPVersion p -> THandleParams SMPVersion p From c7886926870e97fa592d51fa36a2cdec49296388 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:42:59 +0400 Subject: [PATCH 102/125] agent: servers summary types, api (#1202) * agent: servers summary types, api [wip] * encoding * export * Revert "export" This reverts commit cd9f315fe8d3e07344cfb48bac24b8dcf38fdfa1. * comment * rename * simplify types * uncomment * comment * rework * comment, exports * save, restore stats wip * remove * rename * save stats periodically * sigint, sigterm experiments * corrections * remove some proxy stats * increase stat * proposed stats * fields * Revert "sigint, sigterm experiments" This reverts commit f876fbd418889bb59384497d535c91575d7cbb77. * wip * retries -> attempts * errs * fix * other errs * more stat tracking * sub stats * remove xftp successes stats * xftp stats tracking * revert * revert * refactor * remove imports * comment * Revert "refactor" This reverts commit 26c368d82a5bb1366f2b2cb50bdba82f29d6db96. * Revert "revert" This reverts commit 4c9e3753b5251daf39e21c36d356c734431bed50. * Revert "revert" This reverts commit 6f656440538bccd1fbe63cf431b40f3184a523a4. * todos * persistence * rename, fix * config * comment * add started at to summary * delete stats on user deletion * reset api * move * getAgentServersSummary collect state logic * corrections * corrections * remove * rework * decrease contention * update * more stats * count sentProxied * count subs * remove unused * comment * remove comment * comment * export * refactor * cleanup * intervals * refactor * refactor2 * refactor3 * refactor4 --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- simplexmq.cabal | 2 + src/Simplex/FileTransfer/Agent.hs | 21 +- src/Simplex/Messaging/Agent.hs | 165 ++++++--- src/Simplex/Messaging/Agent/Client.hs | 155 +++++++- src/Simplex/Messaging/Agent/Env/SQLite.hs | 4 + src/Simplex/Messaging/Agent/Stats.hs | 337 ++++++++++++++++++ src/Simplex/Messaging/Agent/Store.hs | 2 + src/Simplex/Messaging/Agent/Store/SQLite.hs | 20 ++ .../Agent/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20240518_servers_stats.hs | 29 ++ .../Store/SQLite/Migrations/agent_schema.sql | 7 + src/Simplex/Messaging/Util.hs | 11 +- 12 files changed, 698 insertions(+), 59 deletions(-) create mode 100644 src/Simplex/Messaging/Agent/Stats.hs create mode 100644 src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs diff --git a/simplexmq.cabal b/simplexmq.cabal index 1166ca4ff..2474cebd4 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -95,6 +95,7 @@ library Simplex.Messaging.Agent.Protocol Simplex.Messaging.Agent.QueryString Simplex.Messaging.Agent.RetryInterval + Simplex.Messaging.Agent.Stats Simplex.Messaging.Agent.Store Simplex.Messaging.Agent.Store.SQLite Simplex.Messaging.Agent.Store.SQLite.Common @@ -132,6 +133,7 @@ library Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays + Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats Simplex.Messaging.Agent.TRcvQueues Simplex.Messaging.Client Simplex.Messaging.Client.Agent diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index 890966888..90dda5cbc 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -63,6 +63,7 @@ import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval +import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C @@ -184,6 +185,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do liftIO $ waitForUserNetwork c + atomically $ incXFTPServerStat c userId srv downloadAttempts downloadFileChunk fc replica approvedRelays `catchAgentError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e where @@ -194,7 +196,11 @@ runXFTPRcvWorker c srv Worker {doWork} = do withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay atomically $ assertAgentForeground c loop - retryDone = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) + retryDone e = do + atomically . incXFTPServerStat c userId srv $ case e of + XFTP _ XFTP.AUTH -> downloadAuthErrs + _ -> downloadErrs + rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) e downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM () downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ FILE NOT_APPROVED @@ -214,6 +220,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do Just RcvFileRedirect {redirectFileInfo = RedirectFileInfo {size = FileSize finalSize}, redirectEntityId} -> (redirectEntityId, finalSize) liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived pure (entityId, complete, RFPROG rcvd total) + atomically $ incXFTPServerStat c userId srv downloads notify c entityId progress when complete . lift . void $ getXFTPRcvWorker True c Nothing @@ -484,6 +491,7 @@ runXFTPSndWorker c srv Worker {doWork} = do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do liftIO $ waitForUserNetwork c + atomically $ incXFTPServerStat c userId srv uploadAttempts uploadFileChunk cfg fc replica `catchAgentError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e where @@ -494,7 +502,9 @@ runXFTPSndWorker c srv Worker {doWork} = do withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay atomically $ assertAgentForeground c loop - retryDone = sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) + retryDone e = do + atomically $ incXFTPServerStat c userId srv uploadErrs + sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) e uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> AM () uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica @@ -510,6 +520,7 @@ runXFTPSndWorker c srv Worker {doWork} = do let uploaded = uploadedSize chunks total = totalSize chunks complete = all chunkUploaded chunks + atomically $ incXFTPServerStat c userId srv uploads notify c sndFileEntityId $ SFPROG uploaded total when complete $ do (sndDescr, rcvDescrs) <- sndFileToDescrs sf @@ -651,6 +662,7 @@ runXFTPDelWorker c srv Worker {doWork} = do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do liftIO $ waitForUserNetwork c + atomically $ incXFTPServerStat c userId srv deleteAttempts deleteChunkReplica `catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e where @@ -661,10 +673,13 @@ runXFTPDelWorker c srv Worker {doWork} = do withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay atomically $ assertAgentForeground c loop - retryDone = delWorkerInternalError c deletedSndChunkReplicaId + retryDone e = do + atomically $ incXFTPServerStat c userId srv deleteErrs + delWorkerInternalError c deletedSndChunkReplicaId e deleteChunkReplica = do agentXFTPDeleteChunk c userId replica withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId + atomically $ incXFTPServerStat c userId srv deletions delWorkerInternalError :: AgentClient -> Int64 -> AgentErrorType -> AM () delWorkerInternalError c deletedSndChunkReplicaId e = do diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index f1ab78200..0c433cfed 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -104,6 +104,8 @@ module Simplex.Messaging.Agent rcConnectHost, rcConnectCtrl, rcDiscoverCtrl, + getAgentServersSummary, + resetAgentServersStats, foregroundAgent, suspendAgent, execAgentStoreSQL, @@ -157,6 +159,7 @@ import Simplex.Messaging.Agent.Lock (withLock, withLock') import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval +import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -202,23 +205,57 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = liftIO $ newSMPAgentEnv cfg store >>= runReaderT runAgent where runAgent = do - c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask + currentTs <- liftIO getCurrentTime + c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers currentTs =<< ask t <- runAgentThreads c `forkFinally` const (liftIO $ disconnectAgentClient c) atomically . writeTVar acThread . Just =<< mkWeakThreadId t pure c runAgentThreads c | backgroundMode = run c "subscriber" $ subscriber c - | otherwise = + | otherwise = do + restoreServersStats c raceAny_ [ run c "subscriber" $ subscriber c, run c "runNtfSupervisor" $ runNtfSupervisor c, - run c "cleanupManager" $ cleanupManager c + run c "cleanupManager" $ cleanupManager c, + run c "logServersStats" $ logServersStats c ] + `E.finally` saveServersStats c run AgentClient {subQ, acThread} name a = a `E.catchAny` \e -> whenM (isJust <$> readTVarIO acThread) $ do logError $ "Agent thread " <> name <> " crashed: " <> tshow e atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ CRITICAL True $ show e) +logServersStats :: AgentClient -> AM' () +logServersStats c = do + delay <- asks (initialLogStatsDelay . config) + liftIO $ threadDelay' delay + int <- asks (logStatsInterval . config) + forever $ do + saveServersStats c + liftIO $ threadDelay' int + +saveServersStats :: AgentClient -> AM' () +saveServersStats c@AgentClient {subQ, smpServersStats, xftpServersStats} = do + sss <- mapM (lift . getAgentSMPServerStats) =<< readTVarIO smpServersStats + xss <- mapM (lift . getAgentXFTPServerStats) =<< readTVarIO xftpServersStats + let stats = AgentPersistedServerStats {smpServersStats = sss, xftpServersStats = xss} + tryAgentError' (withStore' c (`updateServersStats` stats)) >>= \case + Left e -> atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL $ show e) + Right () -> pure () + +restoreServersStats :: AgentClient -> AM' () +restoreServersStats c@AgentClient {smpServersStats, xftpServersStats, srvStatsStartedAt} = do + tryAgentError' (withStore c getServersStats) >>= \case + Left e -> atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ INTERNAL $ show e) + Right (startedAt, Nothing) -> atomically $ writeTVar srvStatsStartedAt startedAt + Right (startedAt, Just AgentPersistedServerStats {smpServersStats = sss, xftpServersStats = xss}) -> do + atomically $ writeTVar srvStatsStartedAt startedAt + sss' <- mapM (atomically . newAgentSMPServerStats') sss + atomically $ writeTVar smpServersStats sss' + xss' <- mapM (atomically . newAgentXFTPServerStats') xss + atomically $ writeTVar xftpServersStats xss' + disconnectAgentClient :: AgentClient -> IO () disconnectAgentClient c@AgentClient {agentEnv = Env {ntfSupervisor = ns, xftpAgent = xa}} = do closeAgentClient c @@ -550,6 +587,10 @@ rcDiscoverCtrl :: AgentClient -> NonEmpty RCCtrlPairing -> AE (RCCtrlPairing, RC rcDiscoverCtrl AgentClient {agentEnv = Env {multicastSubscribers = subs}} = withExceptT RCP . discoverRCCtrl subs {-# INLINE rcDiscoverCtrl #-} +resetAgentServersStats :: AgentClient -> AE () +resetAgentServersStats c = withAgentEnv c $ resetAgentServersStats' c +{-# INLINE resetAgentServersStats #-} + getAgentStats :: AgentClient -> IO [(AgentStatsKey, Int)] getAgentStats c = readTVarIO (agentStats c) >>= mapM (\(k, cnt) -> (k,) <$> readTVarIO cnt) . M.assocs @@ -581,11 +622,15 @@ createUser' c smp xftp = do pure userId deleteUser' :: AgentClient -> UserId -> Bool -> AM () -deleteUser' c userId delSMPQueues = do +deleteUser' c@AgentClient {smpServersStats, xftpServersStats} userId delSMPQueues = do if delSMPQueues then withStore c (`setUserDeleted` userId) >>= deleteConnectionsAsync_ delUser c False else withStore c (`deleteUserRecord` userId) atomically $ TM.delete userId $ smpServers c + atomically $ TM.delete userId $ xftpServers c + atomically $ modifyTVar' smpServersStats $ M.filterWithKey (\(userId', _) _ -> userId' /= userId) + atomically $ modifyTVar' xftpServersStats $ M.filterWithKey (\(userId', _) _ -> userId' /= userId) + lift $ saveServersStats c where delUser = whenM (withStore' c (`deleteUserWithoutConns` userId)) . atomically $ @@ -701,12 +746,13 @@ newConnSrv c userId connId hasNewConn enableNtfs cMode clientData pqInitKeys sub newRcvConnSrv c userId connId' enableNtfs cMode clientData pqInitKeys subMode srv newRcvConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) -newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv = do +newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srvWithAuth@(ProtoServerWithAuth srv _) = do case (cMode, pqInitKeys) of (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwE e + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srvWithAuth smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwE e + atomically $ incSMPServerStat c userId srv connCreated rq' <- withStore c $ \db -> updateNewConnRcv db connId rq lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId when enableNtfs $ do @@ -1121,6 +1167,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do ICDeleteRcvQueue rId -> withServer $ \srv -> tryWithLock "ICDeleteRcvQueue" $ do rq <- withStore c (\db -> getDeletedRcvQueue db connId srv rId) deleteQueue c rq + atomically $ incSMPServerStat c userId srv connDeleted withStore' c (`deleteConnRcvQueue` rq) ICQSecure rId senderKey -> withServer $ \srv -> tryWithLock "ICQSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> @@ -1129,6 +1176,9 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do case find ((replaceQId ==) . dbQId) rqs of Just rq1 -> when (status == Confirmed) $ do secureQueue c rq' senderKey + -- we may add more statistics special to queue rotation later on, + -- not accounting secure during rotation for now: + -- atomically $ incSMPServerStat c userId server connSecured withStore' c $ \db -> setRcvQueueStatus db rq' Secured void . enqueueMessages c cData sqs SMP.noMsgFlags $ QUSE [((server, sndId), True)] rq1' <- withStore' c $ \db -> setRcvSwitchStatus db rq1 $ Just RSSendingQUSE @@ -1164,8 +1214,9 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do rq <- withStore c $ \db -> getRcvQueue db connId srv rId ackQueueMessage c rq srvMsgId secure :: RcvQueue -> SMP.SndPublicAuthKey -> AM () - secure rq senderKey = do + secure rq@RcvQueue {server} senderKey = do secureQueue c rq senderKey + atomically $ incSMPServerStat c userId server connSecured withStore' c $ \db -> setRcvQueueStatus db rq Secured where withServer a = case server_ of @@ -1281,7 +1332,7 @@ submitPendingMsg c cData sq = do void $ getDeliveryWorker True c cData sq runSmpQueueMsgDelivery :: AgentClient -> ConnData -> SndQueue -> (Worker, TMVar ()) -> AM () -runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork}, qLock) = do +runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userId, server} (Worker {doWork}, qLock) = do AgentConfig {messageRetryInterval = ri, messageTimeout, helloTimeout, quotaExceededTimeout} <- asks config forever $ do atomically $ endAgentOperation c AOSndNetwork @@ -1304,36 +1355,40 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork Left e -> do let err = if msgType == AM_A_MSG_ then MERR mId e else ERR e case e of - SMP _ SMP.QUOTA -> case msgType of - AM_CONN_INFO -> connError msgId NOT_AVAILABLE - AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE - _ -> do - expireTs <- addUTCTime (-quotaExceededTimeout) <$> liftIO getCurrentTime - if internalTs < expireTs - then notifyDelMsgs msgId e expireTs - else do - notify $ MWARN (unId msgId) e - retrySndMsg RISlow - SMP _ SMP.AUTH -> case msgType of - AM_CONN_INFO -> connError msgId NOT_AVAILABLE - AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE - AM_RATCHET_INFO -> connError msgId NOT_AVAILABLE - -- in duplexHandshake mode (v2) HELLO is only sent once, without retrying, - -- because the queue must be secured by the time the confirmation or the first HELLO is received - AM_HELLO_ -> case rq_ of - -- party initiating connection - Just _ -> connError msgId NOT_AVAILABLE - -- party joining connection - _ -> connError msgId NOT_ACCEPTED - AM_REPLY_ -> notifyDel msgId err - AM_A_MSG_ -> notifyDel msgId err - AM_A_RCVD_ -> notifyDel msgId err - AM_QCONT_ -> notifyDel msgId err - AM_QADD_ -> qError msgId "QADD: AUTH" - AM_QKEY_ -> qError msgId "QKEY: AUTH" - AM_QUSE_ -> qError msgId "QUSE: AUTH" - AM_QTEST_ -> qError msgId "QTEST: AUTH" - AM_EREADY_ -> notifyDel msgId err + SMP _ SMP.QUOTA -> do + atomically $ incSMPServerStat c userId server sentQuotaErrs + case msgType of + AM_CONN_INFO -> connError msgId NOT_AVAILABLE + AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE + _ -> do + expireTs <- addUTCTime (-quotaExceededTimeout) <$> liftIO getCurrentTime + if internalTs < expireTs + then notifyDelMsgs msgId e expireTs + else do + notify $ MWARN (unId msgId) e + retrySndMsg RISlow + SMP _ SMP.AUTH -> do + atomically $ incSMPServerStat c userId server sentAuthErrs + case msgType of + AM_CONN_INFO -> connError msgId NOT_AVAILABLE + AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE + AM_RATCHET_INFO -> connError msgId NOT_AVAILABLE + -- in duplexHandshake mode (v2) HELLO is only sent once, without retrying, + -- because the queue must be secured by the time the confirmation or the first HELLO is received + AM_HELLO_ -> case rq_ of + -- party initiating connection + Just _ -> connError msgId NOT_AVAILABLE + -- party joining connection + _ -> connError msgId NOT_ACCEPTED + AM_REPLY_ -> notifyDel msgId err + AM_A_MSG_ -> notifyDel msgId err + AM_A_RCVD_ -> notifyDel msgId err + AM_QCONT_ -> notifyDel msgId err + AM_QADD_ -> qError msgId "QADD: AUTH" + AM_QKEY_ -> qError msgId "QKEY: AUTH" + AM_QUSE_ -> qError msgId "QUSE: AUTH" + AM_QTEST_ -> qError msgId "QTEST: AUTH" + AM_EREADY_ -> notifyDel msgId err _ -- for other operations BROKER HOST is treated as a permanent error (e.g., when connecting to the server), -- the message sending would be retried @@ -1345,7 +1400,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork else do when (serverHostError e) $ notify $ MWARN (unId msgId) e retrySndMsg RIFast - | otherwise -> notifyDel msgId err + | otherwise -> do + atomically $ incSMPServerStat c userId server sentOtherErrs + notifyDel msgId err where retrySndMsg riMode = do withStore' c $ \db -> updatePendingMsgRIState db connId msgId riState @@ -1369,7 +1426,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork -- it would lead to the non-deterministic internal ID of the first sent message, at to some other race conditions, -- because it can be sent before HELLO is received -- With `status == Active` condition, CON is sent here only by the accepting party, that previously received HELLO - when (status == Active) $ notify $ CON pqEncryption + when (status == Active) $ do + atomically $ incSMPServerStat c userId server connCompleted + notify $ CON pqEncryption -- this branch should never be reached as receive queue is created before the confirmation, _ -> logError "HELLO sent without receive queue" AM_A_MSG_ -> notify $ SENT mId proxySrv_ @@ -1422,6 +1481,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork forM_ (L.nonEmpty msgIds_) $ \msgIds -> do notify $ MERRS (L.map unId msgIds) err withStore' c $ \db -> forM_ msgIds $ \msgId' -> deleteSndMsgDelivery db connId sq msgId' False `catchAll_` pure () + atomically $ incSMPServerStat' c userId server sentExpiredErrs (length msgIds_ + 1) delMsg :: InternalId -> AM () delMsg = delMsgKeep False delMsgKeep :: Bool -> InternalId -> AM () @@ -1940,6 +2000,12 @@ setNtfServers :: AgentClient -> [NtfServer] -> IO () setNtfServers c = atomically . writeTVar (ntfServers c) {-# INLINE setNtfServers #-} +resetAgentServersStats' :: AgentClient -> AM () +resetAgentServersStats' c@AgentClient {smpServersStats, xftpServersStats} = do + atomically $ TM.clear smpServersStats + atomically $ TM.clear xftpServersStats + withStore' c resetServersStats + -- | Activate operations foregroundAgent :: AgentClient -> IO () foregroundAgent c = do @@ -2108,13 +2174,15 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) Left e -> notify' connId (ERR e) Right () -> pure () processSubOk :: RcvQueue -> TVar [ConnId] -> AM () - processSubOk rq@RcvQueue {connId} upConnIds = + processSubOk rq@RcvQueue {userId, connId} upConnIds = do atomically . whenM (isPendingSub connId) $ do addSubscription c rq modifyTVar' upConnIds (connId :) + atomically $ incSMPServerStat c userId srv connSubscribed processSubErr :: RcvQueue -> SMPClientError -> AM () - processSubErr rq@RcvQueue {connId} e = do + processSubErr rq@RcvQueue {userId, connId} e = do atomically . whenM (isPendingSub connId) $ failSubscription c rq e + atomically $ incSMPServerStat c userId srv connSubErrs lift $ notifyErr connId e isPendingSub connId = (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> AEvent e -> m () @@ -2128,7 +2196,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} smpMsg = withConnLock c connId "processSMP" $ case smpMsg of - SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> + SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> do + atomically $ incSMPServerStat c userId srv recvMsgs void . handleNotifyAck $ do msg' <- decryptSMPMessage rq msg ack' <- handleNotifyAck $ case msg' of @@ -2212,6 +2281,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) Right Nothing -> prohibited "msg: bad agent msg" >> ack Left e@(AGENT A_DUPLICATE) -> do atomically updateDupMsgCount + atomically $ incSMPServerStat c userId srv recvDuplicates withStore' c (\db -> getLastMsg db connId srvMsgId) >>= \case Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck} | userAck -> ackDel internalId @@ -2224,6 +2294,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) _ -> ack _ -> checkDuplicateHash e encryptedMsgHash >> ack Left (AGENT (A_CRYPTO e)) -> do + atomically $ incSMPServerStat c userId srv recvCryptoErrs exists <- withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash unless exists notifySync ack @@ -2236,7 +2307,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) conn'' = updateConnection cData'' connDuplex notify . RSYNC rss' (Just e) $ connectionStats conn'' withStore' c $ \db -> setConnRatchetSync db connId rss' - Left e -> checkDuplicateHash e encryptedMsgHash >> ack + Left e -> do + atomically $ incSMPServerStat c userId srv recvErrs + checkDuplicateHash e encryptedMsgHash >> ack where checkDuplicateHash :: AgentErrorType -> ByteString -> AM () checkDuplicateHash e encryptedMsgHash = @@ -2400,7 +2473,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) -- `sndStatus == Active` when HELLO was previously sent, and this is the reply HELLO -- this branch is executed by the accepting party in duplexHandshake mode (v2) -- (was executed by initiating party in v1 that is no longer supported) - | sndStatus == Active -> notify $ CON pqEncryption + | sndStatus == Active -> do + atomically $ incSMPServerStat c userId srv connCompleted + notify $ CON pqEncryption | otherwise -> enqueueDuplexHello sq _ -> pure () where diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 2ffd6e3a4..ec8424745 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1,4 +1,5 @@ {-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} @@ -88,6 +89,10 @@ module Simplex.Messaging.Agent.Client activeClientSession, agentClientStore, agentDRG, + AgentServersSummary (..), + ServerSessions (..), + SMPServerSubs (..), + getAgentServersSummary, getAgentSubscriptions, slowNetworkConfig, protocolClientError, @@ -135,6 +140,9 @@ module Simplex.Messaging.Agent.Client getNextServer, withUserServers, withNextSrv, + incSMPServerStat, + incSMPServerStat', + incXFTPServerStat, AgentWorkersDetails (..), getAgentWorkersDetails, AgentWorkersSummary (..), @@ -174,7 +182,7 @@ import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (isJust, isNothing, listToMaybe) +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) @@ -196,6 +204,7 @@ import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval +import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), withTransaction) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -315,7 +324,10 @@ data AgentClient = AgentClient agentStats :: TMap AgentStatsKey (TVar Int), msgCounts :: TMap ConnId (TVar (Int, Int)), -- (total, duplicates) clientId :: Int, - agentEnv :: Env + agentEnv :: Env, + smpServersStats :: TMap (UserId, SMPServer) AgentSMPServerStats, + xftpServersStats :: TMap (UserId, XFTPServer) AgentXFTPServerStats, + srvStatsStartedAt :: TVar UTCTime } data SMPConnectedClient = SMPConnectedClient @@ -443,8 +455,8 @@ data UserNetworkType = UNNone | UNCellular | UNWifi | UNEthernet | UNOther deriving (Eq, Show) -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. -newAgentClient :: Int -> InitialAgentServers -> Env -> STM AgentClient -newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = do +newAgentClient :: Int -> InitialAgentServers -> UTCTime -> Env -> STM AgentClient +newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs agentEnv = do let cfg = config agentEnv qSize = tbqSize cfg acThread <- newTVar Nothing @@ -482,6 +494,9 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = smpSubWorkers <- TM.empty agentStats <- TM.empty msgCounts <- TM.empty + smpServersStats <- TM.empty + xftpServersStats <- TM.empty + srvStatsStartedAt <- newTVar currentTs return AgentClient { acThread, @@ -520,7 +535,10 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = agentStats, msgCounts, clientId, - agentEnv + agentEnv, + smpServersStats, + xftpServersStats, + srvStatsStartedAt } slowNetworkConfig :: NetworkConfig -> NetworkConfig @@ -1060,7 +1078,11 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) sendViaProxy destSess@(_, _, qId) = do r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do - liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case + r' <- liftClient SMP (clientServer smp) $ do + atomically $ incSMPServerStat c userId destSrv sentViaProxyAttempts + atomically $ incSMPServerStat c userId (protocolClientServer' smp) sentProxiedAttempts + proxySMPMessage smp proxySess spKey_ senderId msgFlags msg + case r' of Right () -> pure . Just $ protocolClientServer' smp Left proxyErr -> do case proxyErr of @@ -1088,13 +1110,23 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do sameClient smp' = sessionId (thParams smp) == sessionId (thParams smp') sameProxiedRelay proxySess' = prSessionId proxySess == prSessionId proxySess' case r of - Right r' -> pure r' + Right r' -> do + atomically $ incSMPServerStat c userId destSrv sentViaProxy + forM_ r' $ \proxySrv -> atomically $ incSMPServerStat c userId proxySrv sentProxied + pure r' Left e | serverHostError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) | otherwise -> throwE e sendDirectly tSess = - withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> - liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg + withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> do + r <- + tryAgentError $ + liftClient SMP (clientServer smp) $ do + atomically $ incSMPServerStat c userId destSrv sentDirectAttempts + sendSMPMessage smp spKey_ senderId msgFlags msg + case r of + Right () -> atomically $ incSMPServerStat c userId destSrv sentDirect + Left e -> throwE e ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool ipAddressProtected NetworkConfig {socksProxy, hostMode} (ProtocolServer _ hosts _ _) = do @@ -1369,6 +1401,8 @@ subscribeQueues c qs = do pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED "subscribeQueues") else Right rq subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) subscribeQueues_ env session smp qs' = do + let (userId, srv, _) = transportSession' smp + atomically $ incSMPServerStat' c userId srv connSubAttempts (length qs') rs <- sendBatch subscribeSMPQueues smp qs' active <- atomically $ @@ -1916,6 +1950,103 @@ withNextSrv c userId usedSrvs initUsed action = do writeTVar usedSrvs $! used' action srvAuth +incSMPServerStat :: AgentClient -> UserId -> SMPServer -> (AgentSMPServerStats -> TVar Int) -> STM () +incSMPServerStat c userId srv sel = incSMPServerStat' c userId srv sel 1 + +incSMPServerStat' :: AgentClient -> UserId -> SMPServer -> (AgentSMPServerStats -> TVar Int) -> Int -> STM () +incSMPServerStat' AgentClient {smpServersStats} userId srv sel n = do + TM.lookup (userId, srv) smpServersStats >>= \case + Just v -> modifyTVar' (sel v) (+ n) + Nothing -> do + newStats <- newAgentSMPServerStats + modifyTVar' (sel newStats) (+ n) + TM.insert (userId, srv) newStats smpServersStats + +incXFTPServerStat :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int) -> STM () +incXFTPServerStat AgentClient {xftpServersStats} userId srv sel = do + TM.lookup (userId, srv) xftpServersStats >>= \case + Just v -> modifyTVar' (sel v) (+ 1) + Nothing -> do + newStats <- newAgentXFTPServerStats + modifyTVar' (sel newStats) (+ 1) + TM.insert (userId, srv) newStats xftpServersStats + +data AgentServersSummary = AgentServersSummary + { smpServersStats :: Map (UserId, SMPServer) AgentSMPServerStatsData, + xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData, + statsStartedAt :: UTCTime, + smpServersSessions :: Map (UserId, SMPServer) ServerSessions, + smpServersSubs :: Map (UserId, SMPServer) SMPServerSubs, + xftpServersSessions :: Map (UserId, XFTPServer) ServerSessions, + xftpRcvInProgress :: [XFTPServer], + xftpSndInProgress :: [XFTPServer], + xftpDelInProgress :: [XFTPServer] + } + deriving (Show) + +data SMPServerSubs = SMPServerSubs + { ssActive :: Int, -- based on activeSubs + ssPending :: Int -- based on pendingSubs + } + deriving (Show) + +data ServerSessions = ServerSessions + { ssConnected :: Int, + ssErrors :: Int, + ssConnecting :: Int + } + deriving (Show) + +getAgentServersSummary :: AgentClient -> IO AgentServersSummary +getAgentServersSummary c@AgentClient {smpServersStats, xftpServersStats, srvStatsStartedAt, agentEnv} = do + sss <- mapM getAgentSMPServerStats =<< readTVarIO smpServersStats + xss <- mapM getAgentXFTPServerStats =<< readTVarIO xftpServersStats + statsStartedAt <- readTVarIO srvStatsStartedAt + smpServersSessions <- countSessions =<< readTVarIO (smpClients c) + smpServersSubs <- getServerSubs + xftpServersSessions <- countSessions =<< readTVarIO (xftpClients c) + xftpRcvInProgress <- catMaybes <$> getXFTPWorkerSrvs xftpRcvWorkers + xftpSndInProgress <- catMaybes <$> getXFTPWorkerSrvs xftpSndWorkers + xftpDelInProgress <- getXFTPWorkerSrvs xftpDelWorkers + pure + AgentServersSummary + { smpServersStats = sss, + xftpServersStats = xss, + statsStartedAt, + smpServersSessions, + smpServersSubs, + xftpServersSessions, + xftpRcvInProgress, + xftpSndInProgress, + xftpDelInProgress + } + where + getServerSubs = do + subs <- M.foldrWithKey' (addSub incActive) M.empty <$> readTVarIO (getRcvQueues $ activeSubs c) + M.foldrWithKey' (addSub incPending) subs <$> readTVarIO (getRcvQueues $ pendingSubs c) + where + addSub f (userId, srv, _) _ = M.alter (Just . f . fromMaybe SMPServerSubs {ssActive = 0, ssPending = 0}) (userId, srv) + incActive ss = ss {ssActive = ssActive ss + 1} + incPending ss = ss {ssPending = ssPending ss + 1} + Env {xftpAgent = XFTPAgent {xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}} = agentEnv + getXFTPWorkerSrvs workers = foldM addSrv [] . M.toList =<< readTVarIO workers + where + addSrv acc (srv, Worker {doWork}) = do + hasWork <- atomically $ not <$> isEmptyTMVar doWork + pure $ if hasWork then srv : acc else acc + countSessions :: Map (TransportSession msg) (ClientVar msg) -> IO (Map (UserId, ProtoServer msg) ServerSessions) + countSessions = foldM addClient M.empty . M.toList + where + addClient !acc ((userId, srv, _), SessionVar {sessionVar}) = do + c_ <- atomically $ tryReadTMVar sessionVar + pure $ M.alter (Just . add c_) (userId, srv) acc + where + add c_ = modifySessions c_ . fromMaybe ServerSessions {ssConnected = 0, ssErrors = 0, ssConnecting = 0} + modifySessions c_ ss = case c_ of + Just (Right _) -> ss {ssConnected = ssConnected ss + 1} + Just (Left _) -> ss {ssErrors = ssErrors ss + 1} + Nothing -> ss {ssConnecting = ssConnecting ss + 1} + data SubInfo = SubInfo {userId :: UserId, server :: Text, rcvId :: Text, subError :: Maybe String} deriving (Show) @@ -2106,6 +2237,12 @@ $(J.deriveJSON (enumJSON $ dropPrefix "TS") ''ProtocolTestStep) $(J.deriveJSON defaultJSON ''ProtocolTestFailure) +$(J.deriveJSON defaultJSON ''ServerSessions) + +$(J.deriveJSON defaultJSON ''SMPServerSubs) + +$(J.deriveJSON defaultJSON ''AgentServersSummary) + $(J.deriveJSON defaultJSON ''SubInfo) $(J.deriveJSON defaultJSON ''SubscriptionsInfo) diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index ee2bb16cc..2ae2ad5c0 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -100,6 +100,8 @@ data AgentConfig = AgentConfig persistErrorInterval :: NominalDiffTime, initialCleanupDelay :: Int64, cleanupInterval :: Int64, + initialLogStatsDelay :: Int64, + logStatsInterval :: Int64, cleanupStepInterval :: Int, maxWorkerRestartsPerMin :: Int, storedMsgDataTTL :: NominalDiffTime, @@ -170,6 +172,8 @@ defaultAgentConfig = persistErrorInterval = 3, -- seconds initialCleanupDelay = 30 * 1000000, -- 30 seconds cleanupInterval = 30 * 60 * 1000000, -- 30 minutes + initialLogStatsDelay = 10 * 1000000, -- 10 seconds + logStatsInterval = 10 * 1000000, -- 10 seconds cleanupStepInterval = 200000, -- 200ms maxWorkerRestartsPerMin = 5, storedMsgDataTTL = 21 * nominalDay, diff --git a/src/Simplex/Messaging/Agent/Stats.hs b/src/Simplex/Messaging/Agent/Stats.hs new file mode 100644 index 000000000..c8f81a6aa --- /dev/null +++ b/src/Simplex/Messaging/Agent/Stats.hs @@ -0,0 +1,337 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Messaging.Agent.Stats where + +import qualified Data.Aeson.TH as J +import Data.Map (Map) +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +import Simplex.Messaging.Agent.Protocol (UserId) +import Simplex.Messaging.Parsers (defaultJSON, fromTextField_) +import Simplex.Messaging.Protocol (SMPServer, XFTPServer) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) +import UnliftIO.STM + +data AgentSMPServerStats = AgentSMPServerStats + { sentDirect :: TVar Int, -- successfully sent messages + sentViaProxy :: TVar Int, -- successfully sent messages via proxy + sentProxied :: TVar Int, -- successfully sent messages to other destination server via this as proxy + sentDirectAttempts :: TVar Int, -- direct sending attempts (min 1 for each sent message) + sentViaProxyAttempts :: TVar Int, -- proxy sending attempts + sentProxiedAttempts :: TVar Int, -- attempts sending to other destination server via this as proxy + sentAuthErrs :: TVar Int, -- send AUTH errors + sentQuotaErrs :: TVar Int, -- send QUOTA permanent errors (message expired) + sentExpiredErrs :: TVar Int, -- send expired errors + sentOtherErrs :: TVar Int, -- other send permanent errors (excluding above) + recvMsgs :: TVar Int, -- total messages received + recvDuplicates :: TVar Int, -- duplicate messages received + recvCryptoErrs :: TVar Int, -- message decryption errors + recvErrs :: TVar Int, -- receive errors + connCreated :: TVar Int, + connSecured :: TVar Int, + connCompleted :: TVar Int, + connDeleted :: TVar Int, + connSubscribed :: TVar Int, -- total successful subscription + connSubAttempts :: TVar Int, -- subscription attempts + connSubErrs :: TVar Int -- permanent subscription errors (temporary accounted for in attempts) + } + +data AgentSMPServerStatsData = AgentSMPServerStatsData + { _sentDirect :: Int, + _sentViaProxy :: Int, + _sentProxied :: Int, + _sentDirectAttempts :: Int, + _sentViaProxyAttempts :: Int, + _sentProxiedAttempts :: Int, + _sentAuthErrs :: Int, + _sentQuotaErrs :: Int, + _sentExpiredErrs :: Int, + _sentOtherErrs :: Int, + _recvMsgs :: Int, + _recvDuplicates :: Int, + _recvCryptoErrs :: Int, + _recvErrs :: Int, + _connCreated :: Int, + _connSecured :: Int, + _connCompleted :: Int, + _connDeleted :: Int, + _connSubscribed :: Int, + _connSubAttempts :: Int, + _connSubErrs :: Int + } + deriving (Show) + +newAgentSMPServerStats :: STM AgentSMPServerStats +newAgentSMPServerStats = do + sentDirect <- newTVar 0 + sentViaProxy <- newTVar 0 + sentProxied <- newTVar 0 + sentDirectAttempts <- newTVar 0 + sentViaProxyAttempts <- newTVar 0 + sentProxiedAttempts <- newTVar 0 + sentAuthErrs <- newTVar 0 + sentQuotaErrs <- newTVar 0 + sentExpiredErrs <- newTVar 0 + sentOtherErrs <- newTVar 0 + recvMsgs <- newTVar 0 + recvDuplicates <- newTVar 0 + recvCryptoErrs <- newTVar 0 + recvErrs <- newTVar 0 + connCreated <- newTVar 0 + connSecured <- newTVar 0 + connCompleted <- newTVar 0 + connDeleted <- newTVar 0 + connSubscribed <- newTVar 0 + connSubAttempts <- newTVar 0 + connSubErrs <- newTVar 0 + pure + AgentSMPServerStats + { sentDirect, + sentViaProxy, + sentProxied, + sentDirectAttempts, + sentViaProxyAttempts, + sentProxiedAttempts, + sentAuthErrs, + sentQuotaErrs, + sentExpiredErrs, + sentOtherErrs, + recvMsgs, + recvDuplicates, + recvCryptoErrs, + recvErrs, + connCreated, + connSecured, + connCompleted, + connDeleted, + connSubscribed, + connSubAttempts, + connSubErrs + } + +newAgentSMPServerStats' :: AgentSMPServerStatsData -> STM AgentSMPServerStats +newAgentSMPServerStats' s = do + sentDirect <- newTVar $ _sentDirect s + sentViaProxy <- newTVar $ _sentViaProxy s + sentProxied <- newTVar $ _sentProxied s + sentDirectAttempts <- newTVar $ _sentDirectAttempts s + sentViaProxyAttempts <- newTVar $ _sentViaProxyAttempts s + sentProxiedAttempts <- newTVar $ _sentProxiedAttempts s + sentAuthErrs <- newTVar $ _sentAuthErrs s + sentQuotaErrs <- newTVar $ _sentQuotaErrs s + sentExpiredErrs <- newTVar $ _sentExpiredErrs s + sentOtherErrs <- newTVar $ _sentOtherErrs s + recvMsgs <- newTVar $ _recvMsgs s + recvDuplicates <- newTVar $ _recvDuplicates s + recvCryptoErrs <- newTVar $ _recvCryptoErrs s + recvErrs <- newTVar $ _recvErrs s + connCreated <- newTVar $ _connCreated s + connSecured <- newTVar $ _connSecured s + connCompleted <- newTVar $ _connCompleted s + connDeleted <- newTVar $ _connDeleted s + connSubscribed <- newTVar $ _connSubscribed s + connSubAttempts <- newTVar $ _connSubAttempts s + connSubErrs <- newTVar $ _connSubErrs s + pure + AgentSMPServerStats + { sentDirect, + sentViaProxy, + sentProxied, + sentDirectAttempts, + sentViaProxyAttempts, + sentProxiedAttempts, + sentAuthErrs, + sentQuotaErrs, + sentExpiredErrs, + sentOtherErrs, + recvMsgs, + recvDuplicates, + recvCryptoErrs, + recvErrs, + connCreated, + connSecured, + connCompleted, + connDeleted, + connSubscribed, + connSubAttempts, + connSubErrs + } + +-- as this is used to periodically update stats in db, +-- this is not STM to decrease contention with stats updates +getAgentSMPServerStats :: AgentSMPServerStats -> IO AgentSMPServerStatsData +getAgentSMPServerStats s = do + _sentDirect <- readTVarIO $ sentDirect s + _sentViaProxy <- readTVarIO $ sentViaProxy s + _sentProxied <- readTVarIO $ sentProxied s + _sentDirectAttempts <- readTVarIO $ sentDirectAttempts s + _sentViaProxyAttempts <- readTVarIO $ sentViaProxyAttempts s + _sentProxiedAttempts <- readTVarIO $ sentProxiedAttempts s + _sentAuthErrs <- readTVarIO $ sentAuthErrs s + _sentQuotaErrs <- readTVarIO $ sentQuotaErrs s + _sentExpiredErrs <- readTVarIO $ sentExpiredErrs s + _sentOtherErrs <- readTVarIO $ sentOtherErrs s + _recvMsgs <- readTVarIO $ recvMsgs s + _recvDuplicates <- readTVarIO $ recvDuplicates s + _recvCryptoErrs <- readTVarIO $ recvCryptoErrs s + _recvErrs <- readTVarIO $ recvErrs s + _connCreated <- readTVarIO $ connCreated s + _connSecured <- readTVarIO $ connSecured s + _connCompleted <- readTVarIO $ connCompleted s + _connDeleted <- readTVarIO $ connDeleted s + _connSubscribed <- readTVarIO $ connSubscribed s + _connSubAttempts <- readTVarIO $ connSubAttempts s + _connSubErrs <- readTVarIO $ connSubErrs s + pure + AgentSMPServerStatsData + { _sentDirect, + _sentViaProxy, + _sentProxied, + _sentDirectAttempts, + _sentViaProxyAttempts, + _sentProxiedAttempts, + _sentAuthErrs, + _sentQuotaErrs, + _sentExpiredErrs, + _sentOtherErrs, + _recvMsgs, + _recvDuplicates, + _recvCryptoErrs, + _recvErrs, + _connCreated, + _connSecured, + _connCompleted, + _connDeleted, + _connSubscribed, + _connSubAttempts, + _connSubErrs + } + +data AgentXFTPServerStats = AgentXFTPServerStats + { uploads :: TVar Int, -- total replicas uploaded to server + uploadAttempts :: TVar Int, -- upload attempts + uploadErrs :: TVar Int, -- upload errors + downloads :: TVar Int, -- total replicas downloaded from server + downloadAttempts :: TVar Int, -- download attempts + downloadAuthErrs :: TVar Int, -- download AUTH errors + downloadErrs :: TVar Int, -- other download errors (excluding above) + deletions :: TVar Int, -- total replicas deleted from server + deleteAttempts :: TVar Int, -- delete attempts + deleteErrs :: TVar Int -- delete errors + } + +data AgentXFTPServerStatsData = AgentXFTPServerStatsData + { _uploads :: Int, + _uploadAttempts :: Int, + _uploadErrs :: Int, + _downloads :: Int, + _downloadAttempts :: Int, + _downloadAuthErrs :: Int, + _downloadErrs :: Int, + _deletions :: Int, + _deleteAttempts :: Int, + _deleteErrs :: Int + } + deriving (Show) + +newAgentXFTPServerStats :: STM AgentXFTPServerStats +newAgentXFTPServerStats = do + uploads <- newTVar 0 + uploadAttempts <- newTVar 0 + uploadErrs <- newTVar 0 + downloads <- newTVar 0 + downloadAttempts <- newTVar 0 + downloadAuthErrs <- newTVar 0 + downloadErrs <- newTVar 0 + deletions <- newTVar 0 + deleteAttempts <- newTVar 0 + deleteErrs <- newTVar 0 + pure + AgentXFTPServerStats + { uploads, + uploadAttempts, + uploadErrs, + downloads, + downloadAttempts, + downloadAuthErrs, + downloadErrs, + deletions, + deleteAttempts, + deleteErrs + } + +newAgentXFTPServerStats' :: AgentXFTPServerStatsData -> STM AgentXFTPServerStats +newAgentXFTPServerStats' s = do + uploads <- newTVar $ _uploads s + uploadAttempts <- newTVar $ _uploadAttempts s + uploadErrs <- newTVar $ _uploadErrs s + downloads <- newTVar $ _downloads s + downloadAttempts <- newTVar $ _downloadAttempts s + downloadAuthErrs <- newTVar $ _downloadAuthErrs s + downloadErrs <- newTVar $ _downloadErrs s + deletions <- newTVar $ _deletions s + deleteAttempts <- newTVar $ _deleteAttempts s + deleteErrs <- newTVar $ _deleteErrs s + pure + AgentXFTPServerStats + { uploads, + uploadAttempts, + uploadErrs, + downloads, + downloadAttempts, + downloadAuthErrs, + downloadErrs, + deletions, + deleteAttempts, + deleteErrs + } + +-- as this is used to periodically update stats in db, +-- this is not STM to decrease contention with stats updates +getAgentXFTPServerStats :: AgentXFTPServerStats -> IO AgentXFTPServerStatsData +getAgentXFTPServerStats s = do + _uploads <- readTVarIO $ uploads s + _uploadAttempts <- readTVarIO $ uploadAttempts s + _uploadErrs <- readTVarIO $ uploadErrs s + _downloads <- readTVarIO $ downloads s + _downloadAttempts <- readTVarIO $ downloadAttempts s + _downloadAuthErrs <- readTVarIO $ downloadAuthErrs s + _downloadErrs <- readTVarIO $ downloadErrs s + _deletions <- readTVarIO $ deletions s + _deleteAttempts <- readTVarIO $ deleteAttempts s + _deleteErrs <- readTVarIO $ deleteErrs s + pure + AgentXFTPServerStatsData + { _uploads, + _uploadAttempts, + _uploadErrs, + _downloads, + _downloadAttempts, + _downloadAuthErrs, + _downloadErrs, + _deletions, + _deleteAttempts, + _deleteErrs + } + +-- Type for gathering both smp and xftp stats across all users and servers, +-- to then be persisted to db as a single json. +data AgentPersistedServerStats = AgentPersistedServerStats + { smpServersStats :: Map (UserId, SMPServer) AgentSMPServerStatsData, + xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData + } + deriving (Show) + +$(J.deriveJSON defaultJSON ''AgentSMPServerStatsData) + +$(J.deriveJSON defaultJSON ''AgentXFTPServerStatsData) + +$(J.deriveJSON defaultJSON ''AgentPersistedServerStats) + +instance ToField AgentPersistedServerStats where + toField = toField . encodeJSON + +instance FromField AgentPersistedServerStats where + fromField = fromTextField_ decodeJSON diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index 807ca223a..b9ffecbbd 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -635,4 +635,6 @@ data StoreError SEDeletedSndChunkReplicaNotFound | -- | Error when reading work item that suspends worker - do not use! SEWorkItemError ByteString + | -- | Servers stats not found. + SEServersStatsNotFound deriving (Eq, Show, Exception) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index a8b18c5b7..434344c89 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -210,6 +210,10 @@ module Simplex.Messaging.Agent.Store.SQLite deleteDeletedSndChunkReplica, getPendingDelFilesServers, deleteDeletedSndChunkReplicasExpired, + -- Stats + updateServersStats, + getServersStats, + resetServersStats, -- * utilities withConnection, @@ -263,6 +267,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..)) import Simplex.FileTransfer.Types import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval (RI2State (..)) +import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite.Common import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -3017,6 +3022,21 @@ deleteDeletedSndChunkReplicasExpired db ttl = do cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime DB.execute db "DELETE FROM deleted_snd_chunk_replicas WHERE created_at < ?" (Only cutoffTs) +updateServersStats :: DB.Connection -> AgentPersistedServerStats -> IO () +updateServersStats db stats = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE servers_stats SET servers_stats = ?, updated_at = ? WHERE servers_stats_id = 1" (stats, updatedAt) + +getServersStats :: DB.Connection -> IO (Either StoreError (UTCTime, Maybe AgentPersistedServerStats)) +getServersStats db = + firstRow id SEServersStatsNotFound $ + DB.query_ db "SELECT started_at, servers_stats FROM servers_stats WHERE servers_stats_id = 1" + +resetServersStats :: DB.Connection -> IO () +resetServersStats db = do + currentTs <- getCurrentTime + DB.execute db "UPDATE servers_stats SET servers_stats = NULL, started_at = ?, updated_at = ? WHERE servers_stats_id = 1" (currentTs, currentTs) + $(J.deriveJSON defaultJSON ''UpMigration) $(J.deriveToJSON (sumTypeJSON $ dropPrefix "ME") ''MigrationError) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index 5a5ed5b5b..340063f5c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -72,6 +72,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays +import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Transport.Client (TransportHost) @@ -112,7 +113,8 @@ schemaMigrations = ("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect), ("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery), ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem), - ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays) + ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays), + ("m20240518_servers_stats", m20240518_servers_stats, Just down_m20240518_servers_stats) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs new file mode 100644 index 000000000..fe017e233 --- /dev/null +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +-- servers_stats_id: dummy id, there should always only be one record with servers_stats_id = 1 +-- servers_stats: overall accumulated stats, past and session, reset to null on stats reset +-- started_at: starting point of tracking stats, reset on stats reset +m20240518_servers_stats :: Query +m20240518_servers_stats = + [sql| +CREATE TABLE servers_stats( + servers_stats_id INTEGER PRIMARY KEY, + servers_stats TEXT, + started_at TEXT NOT NULL DEFAULT(datetime('now')), + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); + +INSERT INTO servers_stats (servers_stats_id) VALUES (1); +|] + +down_m20240518_servers_stats :: Query +down_m20240518_servers_stats = + [sql| +DROP TABLE servers_stats; +|] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index caf94418a..50cf6d74a 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -394,6 +394,13 @@ CREATE TABLE processed_ratchet_key_hashes( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); +CREATE TABLE servers_stats( + servers_stats_id INTEGER PRIMARY KEY, + servers_stats TEXT, + started_at TEXT NOT NULL DEFAULT(datetime('now')), + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id); CREATE UNIQUE INDEX idx_rcv_queue_id ON rcv_queues(conn_id, rcv_queue_id); CREATE UNIQUE INDEX idx_snd_queue_id ON snd_queues(conn_id, snd_queue_id); diff --git a/src/Simplex/Messaging/Util.hs b/src/Simplex/Messaging/Util.hs index 37557cd23..b023f460a 100644 --- a/src/Simplex/Messaging/Util.hs +++ b/src/Simplex/Messaging/Util.hs @@ -8,16 +8,19 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Trans.Except +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Aeson as J import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Data.Int (Int64) import Data.List (groupBy, sortOn) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeUtf8With) +import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Time (NominalDiffTime) import GHC.Conc (labelThread, myThreadId, threadDelay) import UnliftIO @@ -170,3 +173,9 @@ diffToMilliseconds diff = fromIntegral ((truncate $ diff * 1000) :: Integer) labelMyThread :: MonadIO m => String -> m () labelMyThread label = liftIO $ myThreadId >>= (`labelThread` label) + +encodeJSON :: ToJSON a => a -> Text +encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode + +decodeJSON :: FromJSON a => Text -> Maybe a +decodeJSON = J.decode . LB.fromStrict . encodeUtf8 From 9ee684b0f4051a43d0eee60eb7b9d99ea5262e30 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Jun 2024 08:36:24 +0100 Subject: [PATCH 103/125] rfc: faster handshake protocol (#1203) * rfc: faster handshake protocol * update * 1 message * SKEY * use SKEY for both parties * test * update doc * NEW command parameter * add k=s param to queue URI * fix * add sndSecure field to queues * make sender key non-optional in SndQueue (WIP, tests fail) * fast handshake sometimes works (many tests fail) * correctly handle SKEY retries, avoiding to re-generate the keys * handle SKEY retries during async connection * fix most tests (1 test fails) * remove do * fix contact requests encoding/tests * export * fix: ignore duplicate confirmations, fixes testBatchedPendingMessages * do not store sndSecure in store log if it is false to allow server downgrade * add connection invitation encoding tests --- .../duplex-messaging/duplex-creating-v6.mmd | 63 ++ rfcs/2024-06-14-fast-connection.md | 42 + simplexmq.cabal | 1 + src/Simplex/Messaging/Agent.hs | 210 +++-- src/Simplex/Messaging/Agent/Client.hs | 35 +- src/Simplex/Messaging/Agent/Protocol.hs | 60 +- src/Simplex/Messaging/Agent/QueryString.hs | 7 +- src/Simplex/Messaging/Agent/Store.hs | 9 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 63 +- .../Agent/Store/SQLite/Migrations.hs | 4 +- .../SQLite/Migrations/M20240624_snd_secure.hs | 36 + .../Store/SQLite/Migrations/agent_schema.sql | 4 +- src/Simplex/Messaging/Client.hs | 11 +- src/Simplex/Messaging/Protocol.hs | 50 +- src/Simplex/Messaging/Server.hs | 38 +- src/Simplex/Messaging/Server/QueueStore.hs | 1 + src/Simplex/Messaging/Server/StoreLog.hs | 7 +- src/Simplex/Messaging/Transport.hs | 10 +- tests/AgentTests/ConnectionRequestTests.hs | 296 ++++--- tests/AgentTests/FunctionalAPITests.hs | 793 ++++++++++-------- tests/AgentTests/NotificationTests.hs | 79 +- tests/AgentTests/SQLiteTests.hs | 11 +- tests/CoreTests/TRcvQueuesTests.hs | 1 + tests/NtfClient.hs | 17 +- tests/SMPAgentClient.hs | 6 +- tests/SMPClient.hs | 24 +- tests/SMPProxyTests.hs | 39 +- tests/ServerTests.hs | 26 +- tests/XFTPAgent.hs | 12 +- tests/XFTPClient.hs | 8 +- 30 files changed, 1184 insertions(+), 779 deletions(-) create mode 100644 protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd create mode 100644 rfcs/2024-06-14-fast-connection.md create mode 100644 src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240624_snd_secure.hs diff --git a/protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd b/protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd new file mode 100644 index 000000000..183e4bfa9 --- /dev/null +++ b/protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd @@ -0,0 +1,63 @@ +sequenceDiagram + participant A as Alice + participant AA as Alice's
agent + participant AS as Alice's
server + participant BS as Bob's
server + participant BA as Bob's
agent + participant B as Bob + + note over AA, BA: status (receive/send): NONE/NONE + + note over A, AA: 1. request connection
from agent + A ->> AA: NEW: create
duplex connection + + note over AA, AS: 2. create Alice's SMP queue + AA ->> AS: NEW: create SMP queue + AS ->> AA: IDS: SMP queue IDs + note over AA: status: NEW/NONE + + AA ->> A: INV: invitation
to connect + + note over A, B: 3. out-of-band invitation + A ->> B: OOB: invitation to connect + + note over BA, B: 4. accept connection + B ->> BA: JOIN:
via invitation info + note over BA: status: NONE/NEW + + note over BA, AS: 5. secure Alice's SMP queue + BA ->> AS: SKEY: secure queue (this command needs to be proxied) + note over BA: status: NONE/SECURED + + note over BA, BS: 6. create Bob's SMP queue + BA ->> BS: NEW: create SMP queue + BS ->> BA: IDS: SMP queue IDs + note over BA: status: NEW/SECURED + + note over BA, AA: 7. confirm Alice's SMP queue + BA ->> AS: SEND: Bob's info without sender's key (SMP confirmation with reply queues) + note over BA: status: NEW/CONFIRMED + + AS ->> AA: MSG: Bob's info without
sender server key + note over AA: status: CONFIRMED/NEW + AA ->> AS: ACK: confirm message + + note over AA, BS: 8. secure Bob's SMP queue + AA ->> BS: SKEY: secure queue (this command needs to be proxied) + note over BA: status: CONFIRMED/SECURED + + AA ->> BS: SEND: Alice's info without sender's server key (SMP confirmation without reply queues) + note over AA: status: CONFIRMED/CONFIRMED + + note over AA, A: 9. notify Alice
about connection success
(no HELLO needed in v6) + AA ->> A: CON: connected + note over AA: status: ACTIVE/ACTIVE + + BS ->> BA: MSG: Alice's info without
sender's server key + note over BA: status: CONFIRMED/CONFIRMED + BA ->> B: INFO: Alice's info + BA ->> BS: ACK: confirm message + + note over BA, B: 10. notify Bob
about connection success + BA ->> B: CON: connected + note over BA: status: ACTIVE/ACTIVE diff --git a/rfcs/2024-06-14-fast-connection.md b/rfcs/2024-06-14-fast-connection.md new file mode 100644 index 000000000..000f0ef10 --- /dev/null +++ b/rfcs/2024-06-14-fast-connection.md @@ -0,0 +1,42 @@ +# Faster connection establishment + +## Problem + +SMP protocol is unidirectional, and to create a connection users have to agree two messaging queues. + +V1 of handshake protocol required 5 messages and multiple HELLO sent between the users, which consumed a lot of traffic. + +V2 of handshake protocol was optimized to remove multiple HELLO and also REPLY message, thanks to including queue address together with the key to secure this queue into the confirmation message. + +This eliminated unnecessary traffic from repeated HELLOs, but still requires 4 messages in total and 2 times of each client being online. It is perceived by the users as "it didn't work" (because they see "connecting" after using the link) or "we have to be online at the same time" (and even in this case it is slow on bad network). This hurts usability and creates churn of the new users, as unless people are onboarded by the friends who know how the app works, they cannot figure out how to connect. + +Ideally, we want to have handshake protocol design when an accepting user can send messages straight after using the link (their client says "connected") and the initiating client can send messages as soon as it received confirmation message with the profile. + +This RFC proposes modifications to SMP and SMP Agent protocols to reduce the number of required messages to 2 and allows accepting client to send messages straight after using the link (and sending the confirmation), before receiving the profile of the initiating client in the second message, and the initiating client can send the messages straight after processing the confirmation and sending its own confirmation. + +## Solution + +The current protocol design allows additional confirmation step where the initiating client can confirm the connection having received the profile of the sender. We don't use it in the UI - this confirmation is done automatically and unconditionally. + +Instead of requiring the initiating client to secure its queue with sender's key, we can allow the accepting client to secure it with the additional SKEY command. This would avoid "connecting" state but would introduce "Profile unknown" state where the accepting client does not yet have the profile of the initiating client. In this case we could also use the non-optional alias created during the connection (or have something like "Add alias to be able to send messages immediately" and show warning if the user proceeds without it). + +The additional advantage here is that if the queue of the initiating client was removed, the connection will not procede to create additional queue, failing faster. + +These are the proposed changes: + +1. Modify NEW command to add flag allowing sender to secure the queue (it should not be allowed if queue is created for the contact address). +2. Include flag into the invitation link URI and in reply address encoding that queue(s) can be secured by the sender (to avoid coupling with the protocol version and preserve the possibility of the longer handshakes). +3. Add SKEY command to SMP protocol to allow the sender securing the message queue. +4. This command has to be supported by SMP proxy as well, so that the sender does not connect to the recipient's server directly. +5. Accepting client will secure the messaging queue before sending the confirmation to it. +6. Initiating client will secure the messaging queue before sending the confirmation. + +See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd) for the updated handshake protocol. + +Changes to threat model: the attacker who compromised TLS and knows the queue address can block the connection, as the protocol no longer requires the recipient to decrypt the confirmation to secure the queue. + +Possibly, "fast connection" should be an option in Privacy & security settings. + +## Implementation questions + +Currently we store received confirmations in the database, so that the client can confirm them. This becomes unnecessary. diff --git a/simplexmq.cabal b/simplexmq.cabal index 2474cebd4..d46b58dc2 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -134,6 +134,7 @@ library Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats + Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure Simplex.Messaging.Agent.TRcvQueues Simplex.Messaging.Client Simplex.Messaging.Client.Agent diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 0c433cfed..121c1a1f4 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -175,7 +175,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..)) import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) -import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth, sndAuthKeySMPClientVersion) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) @@ -751,7 +751,8 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srvWithAuth smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwE e + let sndSecure = case cMode of SCMInvitation -> True; SCMContact -> False + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srvWithAuth smpClientVRange subMode sndSecure `catchAgentError` \e -> liftIO (print e) >> throwE e atomically $ incSMPServerStat c userId srv connCreated rq' <- withStore c $ \db -> updateNewConnRcv db connId rq lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId @@ -771,7 +772,7 @@ newConnToJoin :: forall c. AgentClient -> UserId -> ConnId -> Bool -> Connection newConnToJoin c userId connId enableNtfs cReq pqSup = case cReq of CRInvitationUri {} -> lift (compatibleInvitationUri cReq) >>= \case - Just (_, (Compatible (CR.E2ERatchetParams v _ _ _)), aVersion) -> create aVersion (Just v) + Just (_, Compatible (CR.E2ERatchetParams v _ _ _), aVersion) -> create aVersion (Just v) Nothing -> throwE $ AGENT A_VERSION CRContactUri {} -> lift (compatibleContactUri cReq) >>= \case @@ -793,10 +794,10 @@ joinConn c userId connId hasNewConn enableNtfs cReq cInfo pqSupport subMode = do _ -> getSMPServer c userId joinConnSrv c userId connId hasNewConn enableNtfs cReq cInfo pqSupport subMode srv -startJoinInvitation :: UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM (ConnData, NewSndQueue, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) -startJoinInvitation userId connId enableNtfs cReqUri pqSup = +startJoinInvitation :: UserId -> ConnId -> Maybe SndQueue -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM (ConnData, NewSndQueue, C.PublicKeyX25519, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) +startJoinInvitation userId connId sq_ enableNtfs cReqUri pqSup = lift (compatibleInvitationUri cReqUri) >>= \case - Just (qInfo, (Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_)), Compatible connAgentVersion) -> do + Just (qInfo, Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_), Compatible connAgentVersion) -> do g <- asks random let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) (pk1, pk2, pKem, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ v kem_ pqSupport) @@ -805,9 +806,13 @@ startJoinInvitation userId connId enableNtfs cReqUri pqSup = maxSupported <- asks $ maxVersion . e2eEncryptVRange . config let rcVs = CR.RatchetVersions {current = v, maxSupported} rc = CR.initSndRatchet rcVs rcDHRr rcDHRs rcParams - q <- lift $ newSndQueue userId "" qInfo + -- this case avoids re-generating queue keys and subsequent failure of SKEY that timed out + -- e2ePubKey is always present, it's Maybe historically + (q, e2ePubKey) <- case sq_ of + Just sq@SndQueue {e2ePubKey = Just k} -> pure ((sq :: SndQueue) {dbQueueId = DBNewQueue}, k) + _ -> lift $ newSndQueue userId "" qInfo let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} - pure (cData, q, rc, e2eSndParams) + pure (cData, q, e2ePubKey, rc, e2eSndParams) Nothing -> throwE $ AGENT A_VERSION connRequestPQSupport :: AgentClient -> PQSupport -> ConnectionRequestUri c -> IO (Maybe (VersionSMPA, PQSupport)) @@ -843,7 +848,7 @@ versionPQSupport_ agentV e2eV_ = PQSupport $ agentV >= pqdrSMPAgentVersion && ma joinConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM ConnId joinConnSrv c userId connId hasNewConn enableNtfs inv@CRInvitationUri {} cInfo pqSup subMode srv = withInvLock c (strEncode inv) "joinConnSrv" $ do - (cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSup + (cData, q, _, rc, e2eSndParams) <- startJoinInvitation userId connId Nothing enableNtfs inv pqSup g <- asks random (connId', sq) <- withStore c $ \db -> runExceptT $ do r@(connId', _) <- @@ -853,7 +858,10 @@ joinConnSrv c userId connId hasNewConn enableNtfs inv@CRInvitationUri {} cInfo p liftIO $ createRatchet db connId' rc pure r let cData' = (cData :: ConnData) {connId = connId'} - tryError (confirmQueue c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case + -- joinConnSrv is only used on user interaction, and its failure is permanent, + -- otherwise we would need to manage retries here to avoid SndQueue recreated with a different key, + -- similar to how joinConnAsync does that. + tryError (secureConfirmQueue c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case Right _ -> pure connId' Left e -> do -- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md @@ -869,17 +877,26 @@ joinConnSrv c userId connId hasNewConn enableNtfs cReqUri@CRContactUri {} cInfo joinConnSrvAsync :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM () joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSupport subMode srv = do - (cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSupport - q' <- withStore c $ \db -> runExceptT $ do - liftIO $ createRatchet db connId rc - ExceptT $ updateNewConnSnd db connId q - confirmQueueAsync c cData q' srv cInfo (Just e2eSndParams) subMode + SomeConn cType conn <- withStore c (`getConn` connId) + case conn of + NewConnection _ -> doJoin Nothing + SndConnection _ sq -> doJoin $ Just sq + _ -> throwE $ CMD PROHIBITED $ "joinConnSrvAsync: bad connection " <> show cType + where + doJoin :: Maybe SndQueue -> AM () + doJoin sq_ = do + (cData, sq, _, rc, e2eSndParams) <- startJoinInvitation userId connId sq_ enableNtfs inv pqSupport + sq' <- withStore c $ \db -> runExceptT $ do + liftIO $ createRatchet db connId rc + maybe (ExceptT $ updateNewConnSnd db connId sq) pure sq_ + secureConfirmQueueAsync c cData sq' srv cInfo (Just e2eSndParams) subMode joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode _pqSupport _srv = do throwE $ CMD PROHIBITED "joinConnSrvAsync" createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do - (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode + let sndSecure = smpClientVersion >= sndAuthKeySMPClientVersion + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode sndSecure let qInfo = toVersionT qUri smpClientVersion rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId @@ -1156,8 +1173,11 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do withStore c $ \db -> runExceptT $ (,) <$> ExceptT (getConn db connId) <*> ExceptT (getAcceptedConfirmation db connId) case conn of RcvConnection cData rq -> do - secure rq senderKey - mapM_ (connectReplyQueues c cData ownConnInfo) (L.nonEmpty $ smpReplyQueues senderConf) + mapM_ (secure rq) senderKey + mapM_ (connectReplyQueues c cData ownConnInfo Nothing) (L.nonEmpty $ smpReplyQueues senderConf) + -- duplex connection is matched to handle SKEY retries + DuplexConnection cData _ (sq :| _) -> + mapM_ (connectReplyQueues c cData ownConnInfo (Just sq)) (L.nonEmpty $ smpReplyQueues senderConf) _ -> throwE $ INTERNAL $ "incorrect connection type " <> show (internalCmdTag cmd) ICDuplexSecure _rId senderKey -> withServer' . tryWithLock "ICDuplexSecure" . withDuplexConn $ \(DuplexConnection cData (rq :| _) (sq :| _)) -> do secure rq senderKey @@ -1332,7 +1352,7 @@ submitPendingMsg c cData sq = do void $ getDeliveryWorker True c cData sq runSmpQueueMsgDelivery :: AgentClient -> ConnData -> SndQueue -> (Worker, TMVar ()) -> AM () -runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userId, server} (Worker {doWork}, qLock) = do +runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userId, server, sndSecure} (Worker {doWork}, qLock) = do AgentConfig {messageRetryInterval = ri, messageTimeout, helloTimeout, quotaExceededTimeout} <- asks config forever $ do atomically $ endAgentOperation c AOSndNetwork @@ -1380,7 +1400,6 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI Just _ -> connError msgId NOT_AVAILABLE -- party joining connection _ -> connError msgId NOT_ACCEPTED - AM_REPLY_ -> notifyDel msgId err AM_A_MSG_ -> notifyDel msgId err AM_A_RCVD_ -> notifyDel msgId err AM_QCONT_ -> notifyDel msgId err @@ -1409,10 +1428,11 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI retrySndOp c $ loop riMode Right proxySrv_ -> do case msgType of - AM_CONN_INFO -> setConfirmed - AM_CONN_INFO_REPLY -> setConfirmed + AM_CONN_INFO + | sndSecure -> notify (CON pqEncryption) >> setStatus Active + | otherwise -> setStatus Confirmed + AM_CONN_INFO_REPLY -> setStatus Confirmed AM_RATCHET_INFO -> pure () - AM_REPLY_ -> pure () AM_HELLO_ -> do withStore' c $ \db -> setSndQueueStatus db sq Active case rq_ of @@ -1469,9 +1489,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI AM_EREADY_ -> pure () delMsgKeep (msgType == AM_A_MSG_) msgId where - setConfirmed = do + setStatus status = do withStore' c $ \db -> do - setSndQueueStatus db sq Confirmed + setSndQueueStatus db sq status when (isJust rq_) $ removeConfirmations db connId where notifyDelMsgs :: InternalId -> AgentErrorType -> UTCTime -> AM () @@ -1558,13 +1578,14 @@ switchConnection' c connId = _ -> throwE $ CMD PROHIBITED "switchConnection: not duplex" switchDuplexConnection :: AgentClient -> Connection 'CDuplex -> RcvQueue -> AM ConnectionStats -switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs sqs) rq@RcvQueue {server, dbQueueId = DBQueueId dbQueueId, sndId} = do +switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId, connAgentVersion} rqs sqs) rq@RcvQueue {server, dbQueueId = DBQueueId dbQueueId, sndId} = do checkRQSwchStatus rq RSSwitchStarted clientVRange <- asks $ smpClientVRange . config -- try to get the server that is different from all queues, or at least from the primary rcv queue srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs) srv' <- if srv == server then getNextServer c userId [server] else pure srvAuth - (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe + let sndSecure = connAgentVersion >= sndAuthKeySMPAgentVersion + (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe sndSecure let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq' lift $ addNewQueueSubscription c rq'' tSess sessId @@ -2191,7 +2212,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) notifyErr connId = notify' connId . ERR . protocolClientError SMP (B.unpack $ strEncode srv) processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM () processSMP - rq@RcvQueue {rcvId = rId, e2ePrivKey, e2eDhSecret, status} + rq@RcvQueue {rcvId = rId, sndSecure, e2ePrivKey, e2eDhSecret, status} conn cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} smpMsg = @@ -2220,7 +2241,10 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let e2eDh = C.dh' e2ePubKey e2ePrivKey decryptClientMessage e2eDh clientMsg >>= \case (SMP.PHConfirmation senderKey, AgentConfirmation {e2eEncryption_, encConnInfo, agentVersion}) -> - smpConfirmation srvMsgId conn senderKey e2ePubKey e2eEncryption_ encConnInfo phVer agentVersion >> ack + smpConfirmation srvMsgId conn (Just senderKey) e2ePubKey e2eEncryption_ encConnInfo phVer agentVersion >> ack + (SMP.PHEmpty, AgentConfirmation {e2eEncryption_, encConnInfo, agentVersion}) + | sndSecure -> smpConfirmation srvMsgId conn Nothing e2ePubKey e2eEncryption_ encConnInfo phVer agentVersion >> ack + | otherwise -> prohibited "handshake: missing sender key" >> ack (SMP.PHEmpty, AgentInvitation {connReq, connInfo}) -> smpInvitation srvMsgId conn connReq connInfo >> ack _ -> prohibited "handshake: incorrect state" >> ack @@ -2348,7 +2372,12 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) pure $ Just (internalId, msgMeta, aMessage, rc) _ -> pure Nothing _ -> prohibited "msg: bad client msg" >> ack - _ -> prohibited "msg: no keys" >> ack + (Just e2eDh, Just _) -> + decryptClientMessage e2eDh clientMsg >>= \case + -- this is a repeated confirmation delivery because ack failed to be sent + (_, AgentConfirmation {}) -> ack + _ -> prohibited "msg: public header" >> ack + (Nothing, Nothing) -> prohibited "msg: no keys" >> ack updateConnVersion :: Connection c -> ConnData -> VersionSMPA -> AM (Connection c) updateConnVersion conn' cData' msgAgentVersion = do aVRange <- asks $ smpAgentVRange . config @@ -2385,8 +2414,10 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) notify :: forall e m. (AEntityI e, MonadIO m) => AEvent e -> m () notify = notify' connId - prohibited :: String -> AM () - prohibited = notify . ERR . AGENT . A_PROHIBITED + prohibited :: Text -> AM () + prohibited s = do + logError $ "prohibited: " <> s + notify . ERR . AGENT $ A_PROHIBITED $ T.unpack s enqueueCmd :: InternalCommand -> AM () enqueueCmd = enqueueCommand c "" connId (Just srv) . AInternalCommand @@ -2413,7 +2444,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) parseMessage :: Encoding a => ByteString -> AM a parseMessage = liftEither . parse smpP (AGENT A_MESSAGE) - smpConfirmation :: SMP.MsgId -> Connection c -> C.APublicAuthKey -> C.PublicKeyX25519 -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> ByteString -> VersionSMPC -> VersionSMPA -> AM () + smpConfirmation :: SMP.MsgId -> Connection c -> Maybe C.APublicAuthKey -> C.PublicKeyX25519 -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> ByteString -> VersionSMPC -> VersionSMPA -> AM () smpConfirmation srvMsgId conn' senderKey e2ePubKey e2eEncryption encConnInfo smpClientVersion agentVersion = do logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config @@ -2436,8 +2467,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) case (agentMsgBody_, skipped) of (Right agentMsgBody, CR.SMDNoChange) -> parseMessage agentMsgBody >>= \case - AgentConnInfoReply smpQueues connInfo -> + AgentConnInfoReply smpQueues connInfo -> do processConf connInfo SMPConfirmation {senderKey, e2ePubKey, connInfo, smpReplyQueues = L.toList smpQueues, smpClientVersion} + withStore' c $ \db -> updateRcvMsgHash db connId 1 (InternalRcvId 0) (C.sha256Hash agentMsgBody) _ -> prohibited "conf: not AgentConnInfoReply" -- including AgentConnInfo, that is prohibited here in v2 where processConf connInfo senderConf = do @@ -2450,14 +2482,21 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) notify $ CONF confId pqSupport' srvs connInfo _ -> prohibited "conf: decrypt error or skipped" -- party accepting connection - (DuplexConnection _ (RcvQueue {smpClientVersion = v'} :| _) _, Nothing) -> do + (DuplexConnection _ (rq'@RcvQueue {smpClientVersion = v'} :| _) _, Nothing) -> do g <- asks random - withStore c (\db -> runExceptT $ agentRatchetDecrypt g db connId encConnInfo) >>= parseMessage . fst >>= \case + (agentMsgBody, pqEncryption) <- withStore c $ \db -> runExceptT $ agentRatchetDecrypt g db connId encConnInfo + parseMessage agentMsgBody >>= \case AgentConnInfo connInfo -> do notify $ INFO pqSupport connInfo let dhSecret = C.dh' e2ePubKey e2ePrivKey - withStore' c $ \db -> setRcvQueueConfirmedE2E db rq dhSecret $ min v' smpClientVersion - enqueueCmd $ ICDuplexSecure rId senderKey + withStore' c $ \db -> do + setRcvQueueConfirmedE2E db rq dhSecret $ min v' smpClientVersion + updateRcvMsgHash db connId 1 (InternalRcvId 0) (C.sha256Hash agentMsgBody) + case senderKey of + Just k -> enqueueCmd $ ICDuplexSecure rId k + Nothing -> do + notify $ CON pqEncryption + withStore' c $ \db -> setRcvQueueStatus db rq' Active _ -> prohibited "conf: not AgentConnInfo" _ -> prohibited "conf: incorrect state" _ -> prohibited "conf: status /= new" @@ -2533,21 +2572,17 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let (delSqs, keepSqs) = L.partition ((Just dbQueueId ==) . dbReplaceQId) sqs case L.nonEmpty keepSqs of Just sqs' -> do - -- move inside case? - sq_@SndQueue {sndPublicKey, e2ePubKey} <- lift $ newSndQueue userId connId qInfo + (sq_@SndQueue {sndPublicKey}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo sq2 <- withStore c $ \db -> do liftIO $ mapM_ (deleteConnSndQueue db connId) delSqs addConnSndQueue db connId (sq_ :: NewSndQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} - case (sndPublicKey, e2ePubKey) of - (Just sndPubKey, Just dhPublicKey) -> do - logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId <> " " <> logSecret (senderId queueAddress) - let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}} - void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPubKey)] - sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY - let sqs'' = updatedQs sq1 sqs' <> [sq2] - conn' = DuplexConnection cData' rqs sqs'' - notify . SWITCH QDSnd SPStarted $ connectionStats conn' - _ -> qError "absent sender keys" + logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId <> " " <> logSecret (senderId queueAddress) + let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}} + void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)] + sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY + let sqs'' = updatedQs sq1 sqs' <> [sq2] + conn' = DuplexConnection cData' rqs sqs'' + notify . SWITCH QDSnd SPStarted $ connectionStats conn' _ -> qError "QADD: won't delete all snd queues in connection" _ -> qError "QADD: replaced queue address is not found in connection" _ -> throwE $ AGENT A_VERSION @@ -2717,23 +2752,30 @@ switchStatusError q expected actual = <> (", expected=" <> show expected) <> (", actual=" <> show actual) -connectReplyQueues :: AgentClient -> ConnData -> ConnInfo -> NonEmpty SMPQueueInfo -> AM () -connectReplyQueues c cData@ConnData {userId, connId} ownConnInfo (qInfo :| _) = do +connectReplyQueues :: AgentClient -> ConnData -> ConnInfo -> Maybe SndQueue -> NonEmpty SMPQueueInfo -> AM () +connectReplyQueues c cData@ConnData {userId, connId} ownConnInfo sq_ (qInfo :| _) = do clientVRange <- asks $ smpClientVRange . config case qInfo `proveCompatible` clientVRange of Nothing -> throwE $ AGENT A_VERSION Just qInfo' -> do - sq <- lift $ newSndQueue userId connId qInfo' - sq' <- withStore c $ \db -> upgradeRcvConnToDuplex db connId sq + -- in case of SKEY retry the connection is already duplex + sq' <- maybe upgradeConn pure sq_ + agentSecureSndQueue c sq' enqueueConfirmation c cData sq' ownConnInfo Nothing + where + upgradeConn = do + (sq, _) <- lift $ newSndQueue userId connId qInfo' + withStore c $ \db -> upgradeRcvConnToDuplex db connId sq -confirmQueueAsync :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () -confirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do +secureConfirmQueueAsync :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () +secureConfirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do + agentSecureSndQueue c sq storeConfirmation c cData sq e2eEncryption_ =<< mkAgentConfirmation c cData sq srv connInfo subMode lift $ submitPendingMsg c cData sq -confirmQueue :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () -confirmQueue c cData@ConnData {connId, connAgentVersion, pqSupport} sq srv connInfo e2eEncryption_ subMode = do +secureConfirmQueue :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () +secureConfirmQueue c cData@ConnData {connId, connAgentVersion, pqSupport} sq srv connInfo e2eEncryption_ subMode = do + agentSecureSndQueue c sq msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode void $ sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed @@ -2742,11 +2784,19 @@ confirmQueue c cData@ConnData {connId, connAgentVersion, pqSupport} sq srv connI mkConfirmation aMessage = do currentE2EVersion <- asks $ maxVersion . e2eEncryptVRange . config withStore c $ \db -> runExceptT $ do - void . liftIO $ updateSndIds db connId + let agentMsgBody = smpEncode aMessage + (_, internalSndId, _) <- liftIO $ updateSndIds db connId + liftIO $ updateSndMsgHash db connId internalSndId (C.sha256Hash agentMsgBody) let pqEnc = CR.pqSupportToEnc pqSupport - (encConnInfo, _) <- agentRatchetEncrypt db cData (smpEncode aMessage) e2eEncConnInfoLength (Just pqEnc) currentE2EVersion + (encConnInfo, _) <- agentRatchetEncrypt db cData agentMsgBody e2eEncConnInfoLength (Just pqEnc) currentE2EVersion pure . smpEncode $ AgentConfirmation {agentVersion = connAgentVersion, e2eEncryption_, encConnInfo} +agentSecureSndQueue :: AgentClient -> SndQueue -> AM () +agentSecureSndQueue c sq@SndQueue {sndSecure, status} = + when (sndSecure && status == New) $ do + secureSndQueue c sq + withStore' c $ \db -> setSndQueueStatus db sq Secured + mkAgentConfirmation :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> SubscriptionMode -> AM AgentMessage mkAgentConfirmation c cData sq srv connInfo subMode = do qInfo <- createReplyQueue c cData sq subMode srv @@ -2822,26 +2872,28 @@ agentRatchetDecrypt' g db connId rc encAgentMsg = do liftIO $ updateRatchet db connId rc' skippedDiff liftEither $ bimap (SEAgentError . cryptoError) (,CR.rcRcvKEM rc') agentMsgBody_ -newSndQueue :: UserId -> ConnId -> Compatible SMPQueueInfo -> AM' NewSndQueue -newSndQueue userId connId (Compatible (SMPQueueInfo smpClientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey = rcvE2ePubDhKey})) = do +newSndQueue :: UserId -> ConnId -> Compatible SMPQueueInfo -> AM' (NewSndQueue, C.PublicKeyX25519) +newSndQueue userId connId (Compatible (SMPQueueInfo smpClientVersion SMPQueueAddress {smpServer, senderId, sndSecure, dhPublicKey = rcvE2ePubDhKey})) = do C.AuthAlg a <- asks $ sndAuthAlg . config g <- asks random (sndPublicKey, sndPrivateKey) <- atomically $ C.generateAuthKeyPair a g (e2ePubKey, e2ePrivKey) <- atomically $ C.generateKeyPair g - pure - SndQueue - { userId, - connId, - server = smpServer, - sndId = senderId, - sndPublicKey = Just sndPublicKey, - sndPrivateKey, - e2eDhSecret = C.dh' rcvE2ePubDhKey e2ePrivKey, - e2ePubKey = Just e2ePubKey, - status = New, - dbQueueId = DBNewQueue, - primary = True, - dbReplaceQueueId = Nothing, - sndSwchStatus = Nothing, - smpClientVersion - } + let sq = + SndQueue + { userId, + connId, + server = smpServer, + sndId = senderId, + sndSecure, + sndPublicKey, + sndPrivateKey, + e2eDhSecret = C.dh' rcvE2ePubDhKey e2ePrivKey, + e2ePubKey = Just e2ePubKey, + status = New, + dbQueueId = DBNewQueue, + primary = True, + dbReplaceQueueId = Nothing, + sndSwchStatus = Nothing, + smpClientVersion + } + pure (sq, e2ePubKey) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index ec8424745..2f48ee17e 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -53,6 +53,7 @@ module Simplex.Messaging.Agent.Client temporaryOrHostError, serverHostError, secureQueue, + secureSndQueue, enableQueueNotifications, enableQueuesNtfs, disableQueueNotifications, @@ -73,7 +74,6 @@ module Simplex.Messaging.Agent.Client agentXFTPUploadChunk, agentXFTPAddRecipients, agentXFTPDeleteChunk, - agentCbEncrypt, agentCbDecrypt, cryptoError, sendAck, @@ -240,6 +240,7 @@ import Simplex.Messaging.Protocol RcvNtfPublicDhKey, SMPMsgMeta (..), SProtocolType (..), + SenderCanSecure, SndPublicAuthKey, SubscriptionMode (..), UserProtocol, @@ -1196,11 +1197,14 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do getProtocolClient g tSess cfg Nothing (\_ -> pure ()) >>= \case Right smp -> do rKeys@(_, rpKey) <- atomically $ C.generateAuthKeyPair ra g - (sKey, _) <- atomically $ C.generateAuthKeyPair sa g + (sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g (dhKey, _) <- atomically $ C.generateKeyPair g r <- runExceptT $ do - SMP.QIK {rcvId} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp rKeys dhKey auth SMSubscribe - liftError (testErr TSSecureQueue) $ secureSMPQueue smp rpKey rcvId sKey + SMP.QIK {rcvId, sndId, sndSecure} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp rKeys dhKey auth SMSubscribe True + liftError (testErr TSSecureQueue) $ + if sndSecure + then secureSndSMPQueue smp spKey sndId sKey + else secureSMPQueue smp rpKey rcvId sKey liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp rpKey rcvId ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient smp pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok @@ -1307,8 +1311,8 @@ getSessionMode :: AgentClient -> IO TransportSessionMode getSessionMode = atomically . fmap sessionMode . getNetworkConfig {-# INLINE getSessionMode #-} -newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) -newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do +newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> SenderCanSecure -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) +newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode senderCanSecure = do C.AuthAlg a <- asks (rcvAuthAlg . config) g <- asks random rKeys@(_, rcvPrivateKey) <- atomically $ C.generateAuthKeyPair a g @@ -1316,9 +1320,9 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do (e2eDhKey, e2ePrivKey) <- atomically $ C.generateKeyPair g logServer "-->" c srv "" "NEW" tSess <- liftIO $ mkTransportSession c userId srv connId - (sessId, QIK {rcvId, sndId, rcvPublicDhKey}) <- + (sessId, QIK {rcvId, sndId, rcvPublicDhKey, sndSecure}) <- withClient c tSess "NEW" $ \(SMPConnectedClient smp _) -> - (sessionId $ thParams smp,) <$> createSMPQueue smp rKeys dhKey auth subMode + (sessionId $ thParams smp,) <$> createSMPQueue smp rKeys dhKey auth subMode senderCanSecure liftIO . logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = RcvQueue @@ -1331,6 +1335,7 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do e2ePrivKey, e2eDhSecret = Nothing, sndId, + sndSecure, status = New, dbQueueId = DBNewQueue, primary = True, @@ -1340,7 +1345,7 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do clientNtfCreds = Nothing, deleteErrors = 0 } - qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey + qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey sndSecure pure (rq, qUri, tSess, sessId) processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> STM () @@ -1524,10 +1529,11 @@ logSecret bs = encode $ B.take 3 bs {-# INLINE logSecret #-} sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM (Maybe SMPServer) -sendConfirmation c sq@SndQueue {userId, server, sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do - let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation +sendConfirmation c sq@SndQueue {userId, server, sndId, sndSecure, sndPublicKey, sndPrivateKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do + let (privHdr, spKey) = if sndSecure then (SMP.PHEmpty, Just sndPrivateKey) else (SMP.PHConfirmation sndPublicKey, Nothing) + clientMsg = SMP.ClientMessage privHdr agentConfirmation msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg - sendOrProxySMPMessage c userId server "" Nothing sndId (MsgFlags {notification = True}) msg + sendOrProxySMPMessage c userId server "" spKey sndId (MsgFlags {notification = True}) msg sendConfirmation _ _ _ = throwE $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database" sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM (Maybe SMPServer) @@ -1568,6 +1574,11 @@ secureQueue c rq@RcvQueue {rcvId, rcvPrivateKey} senderKey = withSMPClient c rq "KEY " $ \smp -> secureSMPQueue smp rcvPrivateKey rcvId senderKey +secureSndQueue :: AgentClient -> SndQueue -> AM () +secureSndQueue c sq@SndQueue {sndId, sndPrivateKey, sndPublicKey} = + withSMPClient c sq "SKEY " $ \smp -> + secureSndSMPQueue smp sndPrivateKey sndId sndPublicKey + enableQueueNotifications :: AgentClient -> RcvQueue -> SMP.NtfPublicAuthKey -> SMP.RcvNtfPublicDhKey -> AM (SMP.NotifierId, SMP.RcvNtfPublicDhKey) enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey = withSMPClient c rq "NKEY " $ \smp -> diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index d6bbc13ca..b123fc1ec 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -41,6 +41,7 @@ module Simplex.Messaging.Agent.Protocol ratchetSyncSMPAgentVersion, deliveryRcptsSMPAgentVersion, pqdrSMPAgentVersion, + sndAuthKeySMPAgentVersion, currentSMPAgentVersion, supportedSMPAgentVRange, e2eEncConnInfoLength, @@ -208,6 +209,7 @@ import Simplex.Messaging.Protocol legacyStrEncodeServer, noAuthSrv, sameSrvAddr, + sndAuthKeySMPClientVersion, srvHostnamesSMPClientVersion, pattern ProtoServerWithAuth, pattern SMPServer, @@ -227,6 +229,7 @@ import UnliftIO.Exception (Exception) -- 3 - support ratchet renegotiation (6/30/2023) -- 4 - delivery receipts (7/13/2023) -- 5 - post-quantum double ratchet (3/14/2024) +-- 6 - secure reply queues with provided keys (6/14/2024) data SMPAgentVersion @@ -251,11 +254,17 @@ deliveryRcptsSMPAgentVersion = VersionSMPA 4 pqdrSMPAgentVersion :: VersionSMPA pqdrSMPAgentVersion = VersionSMPA 5 +sndAuthKeySMPAgentVersion :: VersionSMPA +sndAuthKeySMPAgentVersion = VersionSMPA 6 + +minSupportedSMPAgentVersion :: VersionSMPA +minSupportedSMPAgentVersion = duplexHandshakeSMPAgentVersion + currentSMPAgentVersion :: VersionSMPA -currentSMPAgentVersion = VersionSMPA 5 +currentSMPAgentVersion = VersionSMPA 6 supportedSMPAgentVRange :: VersionRangeSMPA -supportedSMPAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion currentSMPAgentVersion +supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPAgentVersion -- it is shorter to allow all handshake headers, -- including E2E (double-ratchet) parameters and @@ -685,7 +694,7 @@ data MsgMeta = MsgMeta data SMPConfirmation = SMPConfirmation { -- | sender's public key to use for authentication of sender's commands at the recepient's server - senderKey :: SndPublicAuthKey, + senderKey :: Maybe SndPublicAuthKey, -- | sender's DH public key for simple per-queue e2e encryption e2ePubKey :: C.PublicKeyX25519, -- | sender's information to be associated with the connection, e.g. sender's profile information @@ -775,12 +784,12 @@ instance Encoding AgentMessage where 'M' -> AgentMessage <$> smpP <*> smpP _ -> fail "bad AgentMessage" +-- internal type for storing message type in the database data AgentMessageType = AM_CONN_INFO | AM_CONN_INFO_REPLY | AM_RATCHET_INFO | AM_HELLO_ - | AM_REPLY_ | AM_A_MSG_ | AM_A_RCVD_ | AM_QCONT_ @@ -797,7 +806,6 @@ instance Encoding AgentMessageType where AM_CONN_INFO_REPLY -> "D" AM_RATCHET_INFO -> "S" AM_HELLO_ -> "H" - AM_REPLY_ -> "R" AM_A_MSG_ -> "M" AM_A_RCVD_ -> "V" AM_QCONT_ -> "QC" @@ -812,7 +820,6 @@ instance Encoding AgentMessageType where 'D' -> pure AM_CONN_INFO_REPLY 'S' -> pure AM_RATCHET_INFO 'H' -> pure AM_HELLO_ - 'R' -> pure AM_REPLY_ 'M' -> pure AM_A_MSG_ 'V' -> pure AM_A_RCVD_ 'Q' -> @@ -1004,7 +1011,8 @@ instance ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where where queryStr = strEncode . QSP QEscape $ - [("v", strEncode crAgentVRange), ("smp", strEncode crSmpQueues)] + -- semicolon is used to separate SMP queues because comma is used to separate server address hostnames + [("v", strEncode crAgentVRange), ("smp", B.intercalate ";" $ map strEncode $ L.toList crSmpQueues)] <> maybe [] (\e2e -> [("e2e", strEncode e2e)]) e2eParams <> maybe [] (\cd -> [("data", encodeUtf8 cd)]) crClientData strP = connReqUriP' (Just SSSimplex) @@ -1026,7 +1034,7 @@ connReqUriP overrideScheme = do crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?" query <- strP aVRange <- queryParam "v" query - crSmpQueues <- queryParam "smp" query + crSmpQueues <- queryParamParser queuesP "smp" query let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query crData = ConnReqUriData {crScheme, crAgentVRange = aVRange, crSmpQueues, crClientData} case crMode of @@ -1038,8 +1046,10 @@ connReqUriP overrideScheme = do CMContact -> pure . ACR SCMContact $ CRContactUri crData {crAgentVRange = adjustAgentVRange aVRange} where crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact + -- semicolon is used to separate SMP queues because comma is used to separate server address hostnames + queuesP = L.fromList <$> (strDecode <$?> A.takeTill (== ';')) `A.sepBy1'` A.char ';' adjustAgentVRange vr = - let v = max duplexHandshakeSMPAgentVersion $ minVersion vr + let v = max minSupportedSMPAgentVersion $ minVersion vr in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) instance ConnectionModeI m => FromJSON (ConnectionRequestUri m) where @@ -1117,14 +1127,16 @@ data SMPQueueInfo = SMPQueueInfo {clientVersion :: VersionSMPC, queueAddress :: deriving (Eq, Show) instance Encoding SMPQueueInfo where - smpEncode (SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey}) + smpEncode (SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure}) + | clientVersion >= sndAuthKeySMPClientVersion && sndSecure = smpEncode (clientVersion, smpServer, senderId, dhPublicKey, sndSecure) | clientVersion > initialSMPClientVersion = smpEncode (clientVersion, smpServer, senderId, dhPublicKey) | otherwise = smpEncode clientVersion <> legacyEncodeServer smpServer <> smpEncode (senderId, dhPublicKey) smpP = do clientVersion <- smpP smpServer <- if clientVersion > initialSMPClientVersion then smpP else updateSMPServerHosts <$> legacyServerP (senderId, dhPublicKey) <- smpP - pure $ SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey} + sndSecure <- fromMaybe False <$> optional smpP + pure $ SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure} -- This instance seems contrived and there was a temptation to split a common part of both types. -- But this is created to allow backward and forward compatibility where SMPQueueUri @@ -1150,7 +1162,8 @@ data SMPQueueUri = SMPQueueUri {clientVRange :: VersionRangeSMPC, queueAddress : data SMPQueueAddress = SMPQueueAddress { smpServer :: SMPServer, senderId :: SMP.SenderId, - dhPublicKey :: C.PublicKeyX25519 + dhPublicKey :: C.PublicKeyX25519, + sndSecure :: Bool } deriving (Eq, Show) @@ -1177,37 +1190,42 @@ sameQAddress (srv, qId) (srv', qId') = sameSrvAddr srv srv' && qId == qId' {-# INLINE sameQAddress #-} instance StrEncoding SMPQueueUri where - strEncode (SMPQueueUri vr SMPQueueAddress {smpServer = srv, senderId = qId, dhPublicKey}) + strEncode (SMPQueueUri vr SMPQueueAddress {smpServer = srv, senderId = qId, dhPublicKey, sndSecure}) | minVersion vr >= srvHostnamesSMPClientVersion = strEncode srv <> "/" <> strEncode qId <> "#/?" <> query queryParams | otherwise = legacyStrEncodeServer srv <> "/" <> strEncode qId <> "#/?" <> query (queryParams <> srvParam) where query = strEncode . QSP QEscape - queryParams = [("v", strEncode vr), ("dh", strEncode dhPublicKey)] + queryParams = [("v", strEncode vr), ("dh", strEncode dhPublicKey)] <> [("k", "s") | sndSecure] srvParam = [("srv", strEncode $ TransportHosts_ hs) | not (null hs)] hs = L.tail $ host srv strP = do srv@ProtocolServer {host = h :| host} <- strP <* A.char '/' senderId <- strP <* optional (A.char '/') <* A.char '#' - (vr, hs, dhPublicKey) <- unversioned <|> versioned + (vr, hs, dhPublicKey, sndSecure) <- versioned <|> unversioned let srv' = srv {host = h :| host <> hs} smpServer = if maxVersion vr < srvHostnamesSMPClientVersion then updateSMPServerHosts srv' else srv' - pure $ SMPQueueUri vr SMPQueueAddress {smpServer, senderId, dhPublicKey} + pure $ SMPQueueUri vr SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure} where - unversioned = (versionToRange initialSMPClientVersion,[],) <$> strP <* A.endOfInput + unversioned = (versionToRange initialSMPClientVersion,[],,False) <$> strP <* A.endOfInput versioned = do dhKey_ <- optional strP query <- optional (A.char '/') *> A.char '?' *> strP vr <- queryParam "v" query dhKey <- maybe (queryParam "dh" query) pure dhKey_ hs_ <- queryParam_ "srv" query - pure (vr, maybe [] thList_ hs_, dhKey) + let sndSecure = queryParamStr "k" query == Just "s" + pure (vr, maybe [] thList_ hs_, dhKey, sndSecure) instance Encoding SMPQueueUri where - smpEncode (SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey}) = - smpEncode (clientVRange, smpServer, senderId, dhPublicKey) + smpEncode (SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure}) + | maxVersion clientVRange >= sndAuthKeySMPClientVersion && sndSecure = + smpEncode (clientVRange, smpServer, senderId, dhPublicKey, sndSecure) + | otherwise = + smpEncode (clientVRange, smpServer, senderId, dhPublicKey) smpP = do (clientVRange, smpServer, senderId, dhPublicKey) <- smpP - pure $ SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey} + sndSecure <- fromMaybe False <$> optional smpP + pure $ SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure} data ConnectionRequestUri (m :: ConnectionMode) where CRInvitationUri :: ConnReqUriData -> RcvE2ERatchetParamsUri 'C.X448 -> ConnectionRequestUri CMInvitation diff --git a/src/Simplex/Messaging/Agent/QueryString.hs b/src/Simplex/Messaging/Agent/QueryString.hs index fee552a01..9dc0e94a9 100644 --- a/src/Simplex/Messaging/Agent/QueryString.hs +++ b/src/Simplex/Messaging/Agent/QueryString.hs @@ -24,9 +24,12 @@ instance StrEncoding QueryStringParams where strP = QSP QEscape . Q.parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n') queryParam :: StrEncoding a => ByteString -> QueryStringParams -> Parser a -queryParam name q = +queryParam = queryParamParser strP + +queryParamParser :: Parser a -> ByteString -> QueryStringParams -> Parser a +queryParamParser p name q = case queryParamStr name q of - Just p -> either fail pure $ parseAll strP p + Just s -> either fail pure $ parseAll p s _ -> fail $ "no qs param " <> B.unpack name queryParam_ :: StrEncoding a => ByteString -> QueryStringParams -> Parser (Maybe a) diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index b9ffecbbd..baec2ef93 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -44,6 +44,7 @@ import Simplex.Messaging.Protocol RcvPrivateAuthKey, SndPrivateAuthKey, SndPublicAuthKey, + SenderCanSecure, VersionSMPC, ) import qualified Simplex.Messaging.Protocol as SMP @@ -83,6 +84,8 @@ data StoredRcvQueue (q :: QueueStored) = RcvQueue e2eDhSecret :: Maybe C.DhSecretX25519, -- | sender queue ID sndId :: SMP.SenderId, + -- | sender can secure the queue + sndSecure :: SenderCanSecure, -- | queue status status :: QueueStatus, -- | database queue ID (within connection) @@ -138,9 +141,11 @@ data StoredSndQueue (q :: QueueStored) = SndQueue server :: SMPServer, -- | sender queue ID sndId :: SMP.SenderId, + -- | sender can secure the queue + sndSecure :: SenderCanSecure, -- | key pair used by the sender to authorize transmissions -- TODO combine keys to key pair so that types match - sndPublicKey :: Maybe SndPublicAuthKey, + sndPublicKey :: SndPublicAuthKey, sndPrivateKey :: SndPrivateAuthKey, -- | DH public key used to negotiate per-queue e2e encryption e2ePubKey :: Maybe C.PublicKeyX25519, @@ -372,7 +377,7 @@ instance StrEncoding AgentCommandTag where data InternalCommand = ICAck SMP.RecipientId MsgId | ICAckDel SMP.RecipientId MsgId InternalId - | ICAllowSecure SMP.RecipientId SMP.SndPublicAuthKey + | ICAllowSecure SMP.RecipientId (Maybe SMP.SndPublicAuthKey) | ICDuplexSecure SMP.RecipientId SMP.SndPublicAuthKey | ICDeleteConn | ICDeleteRcvQueue SMP.RecipientId diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 434344c89..d4cd99b39 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -102,8 +102,10 @@ module Simplex.Messaging.Agent.Store.SQLite -- Messages updateRcvIds, createRcvMsg, + updateRcvMsgHash, updateSndIds, createSndMsg, + updateSndMsgHash, createSndMsgDelivery, getSndMsgViaRcpt, updateSndMsgRcpt, @@ -811,7 +813,7 @@ setRcvQueueNtfCreds db connId clientNtfCreds = Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret} -> (Just ntfPublicKey, Just ntfPrivateKey, Just notifierId, Just rcvNtfDhSecret) Nothing -> (Nothing, Nothing, Nothing, Nothing) -type SMPConfirmationRow = (SndPublicAuthKey, C.PublicKeyX25519, ConnInfo, Maybe [SMPQueueInfo], Maybe VersionSMPC) +type SMPConfirmationRow = (Maybe SndPublicAuthKey, C.PublicKeyX25519, ConnInfo, Maybe [SMPQueueInfo], Maybe VersionSMPC) smpConfirmation :: SMPConfirmationRow -> SMPConfirmation smpConfirmation (senderKey, e2ePubKey, connInfo, smpReplyQueues_, smpClientVersion_) = @@ -958,10 +960,10 @@ updateRcvIds db connId = do pure (internalId, internalRcvId, lastExternalSndId, lastRcvHash) createRcvMsg :: DB.Connection -> ConnId -> RcvQueue -> RcvMsgData -> IO () -createRcvMsg db connId rq rcvMsgData = do +createRcvMsg db connId rq rcvMsgData@RcvMsgData {msgMeta = MsgMeta {sndMsgId}, internalRcvId, internalHash} = do insertRcvMsgBase_ db connId rcvMsgData insertRcvMsgDetails_ db connId rq rcvMsgData - updateHashRcv_ db connId rcvMsgData + updateRcvMsgHash db connId sndMsgId internalRcvId internalHash updateSndIds :: DB.Connection -> ConnId -> IO (InternalId, InternalSndId, PrevSndMsgHash) updateSndIds db connId = do @@ -972,10 +974,10 @@ updateSndIds db connId = do pure (internalId, internalSndId, prevSndHash) createSndMsg :: DB.Connection -> ConnId -> SndMsgData -> IO () -createSndMsg db connId sndMsgData = do +createSndMsg db connId sndMsgData@SndMsgData {internalSndId, internalHash} = do insertSndMsgBase_ db connId sndMsgData insertSndMsgDetails_ db connId sndMsgData - updateHashSnd_ db connId sndMsgData + updateSndMsgHash db connId internalSndId internalHash createSndMsgDelivery :: DB.Connection -> ConnId -> SndQueue -> InternalId -> IO () createSndMsgDelivery db connId SndQueue {dbQueueId} msgId = @@ -1866,28 +1868,34 @@ upsertNtfServer_ db ProtocolServer {host, port, keyHash} = do insertRcvQueue_ :: DB.Connection -> ConnId -> NewRcvQueue -> Maybe C.KeyHash -> IO RcvQueue insertRcvQueue_ db connId' rq@RcvQueue {..} serverKeyHash_ = do - qId <- newQueueId_ <$> DB.query db "SELECT rcv_queue_id FROM rcv_queues WHERE conn_id = ? ORDER BY rcv_queue_id DESC LIMIT 1" (Only connId') + -- to preserve ID if the queue already exists. + -- possibly, it can be done in one query. + currQId_ <- maybeFirstRow fromOnly $ DB.query db "SELECT rcv_queue_id FROM rcv_queues WHERE conn_id = ? AND host = ? AND port = ? AND snd_id = ?" (connId', host server, port server, sndId) + qId <- maybe (newQueueId_ <$> DB.query db "SELECT rcv_queue_id FROM rcv_queues WHERE conn_id = ? ORDER BY rcv_queue_id DESC LIMIT 1" (Only connId')) pure currQId_ DB.execute db [sql| INSERT INTO rcv_queues - (host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); + (host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, snd_secure, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); |] - ((host server, port server, rcvId, connId', rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret) :. (sndId, status, qId, primary, dbReplaceQueueId, smpClientVersion, serverKeyHash_)) + ((host server, port server, rcvId, connId', rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret) :. (sndId, sndSecure, status, qId, primary, dbReplaceQueueId, smpClientVersion, serverKeyHash_)) pure (rq :: NewRcvQueue) {connId = connId', dbQueueId = qId} -- * createSndConn helpers insertSndQueue_ :: DB.Connection -> ConnId -> NewSndQueue -> Maybe C.KeyHash -> IO SndQueue insertSndQueue_ db connId' sq@SndQueue {..} serverKeyHash_ = do - qId <- newQueueId_ <$> DB.query db "SELECT snd_queue_id FROM snd_queues WHERE conn_id = ? ORDER BY snd_queue_id DESC LIMIT 1" (Only connId') + -- to preserve ID if the queue already exists. + -- possibly, it can be done in one query. + currQId_ <- maybeFirstRow fromOnly $ DB.query db "SELECT snd_queue_id FROM snd_queues WHERE conn_id = ? AND host = ? AND port = ? AND snd_id = ?" (connId', host server, port server, sndId) + qId <- maybe (newQueueId_ <$> DB.query db "SELECT snd_queue_id FROM snd_queues WHERE conn_id = ? ORDER BY snd_queue_id DESC LIMIT 1" (Only connId')) pure currQId_ DB.execute db [sql| INSERT OR REPLACE INTO snd_queues - (host, port, snd_id, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, status, snd_queue_id, snd_primary, replace_snd_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?); + (host, port, snd_id, snd_secure, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, status, snd_queue_id, snd_primary, replace_snd_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); |] - ((host server, port server, sndId, connId', sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret) :. (status, qId, primary, dbReplaceQueueId, smpClientVersion, serverKeyHash_)) + ((host server, port server, sndId, sndSecure, connId', sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret) :. (status, qId, primary, dbReplaceQueueId, smpClientVersion, serverKeyHash_)) pure (sq :: NewSndQueue) {connId = connId', dbQueueId = qId} newQueueId_ :: [Only Int64] -> DBQueueId 'QSStored @@ -2009,7 +2017,7 @@ rcvQueueQuery :: Query rcvQueueQuery = [sql| SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.status, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret FROM rcv_queues q @@ -2018,17 +2026,17 @@ rcvQueueQuery = |] toRcvQueue :: - (UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SMP.RecipientId, SMP.RcvPrivateAuthKey, SMP.RcvDhSecret, C.PrivateKeyX25519, Maybe C.DhSecretX25519, SMP.SenderId, QueueStatus) - :. (DBQueueId 'QSStored, Bool, Maybe Int64, Maybe RcvSwitchStatus, Maybe VersionSMPC, Int) + (UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SMP.RecipientId, SMP.RcvPrivateAuthKey, SMP.RcvDhSecret, C.PrivateKeyX25519, Maybe C.DhSecretX25519, SMP.SenderId, SenderCanSecure) + :. (QueueStatus, DBQueueId 'QSStored, Bool, Maybe Int64, Maybe RcvSwitchStatus, Maybe VersionSMPC, Int) :. (Maybe SMP.NtfPublicAuthKey, Maybe SMP.NtfPrivateAuthKey, Maybe SMP.NotifierId, Maybe RcvNtfDhSecret) -> RcvQueue -toRcvQueue ((userId, keyHash, connId, host, port, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status) :. (dbQueueId, primary, dbReplaceQueueId, rcvSwchStatus, smpClientVersion_, deleteErrors) :. (ntfPublicKey_, ntfPrivateKey_, notifierId_, rcvNtfDhSecret_)) = +toRcvQueue ((userId, keyHash, connId, host, port, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, sndSecure) :. (status, dbQueueId, primary, dbReplaceQueueId, rcvSwchStatus, smpClientVersion_, deleteErrors) :. (ntfPublicKey_, ntfPrivateKey_, notifierId_, rcvNtfDhSecret_)) = let server = SMPServer host port keyHash smpClientVersion = fromMaybe initialSMPClientVersion smpClientVersion_ clientNtfCreds = case (ntfPublicKey_, ntfPrivateKey_, notifierId_, rcvNtfDhSecret_) of (Just ntfPublicKey, Just ntfPrivateKey, Just notifierId, Just rcvNtfDhSecret) -> Just $ ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret} _ -> Nothing - in RcvQueue {userId, connId, server, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status, dbQueueId, primary, dbReplaceQueueId, rcvSwchStatus, smpClientVersion, clientNtfCreds, deleteErrors} + in RcvQueue {userId, connId, server, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, sndSecure, status, dbQueueId, primary, dbReplaceQueueId, rcvSwchStatus, smpClientVersion, clientNtfCreds, deleteErrors} getRcvQueueById :: DB.Connection -> ConnId -> Int64 -> IO (Either StoreError RcvQueue) getRcvQueueById db connId dbRcvId = @@ -2049,7 +2057,7 @@ sndQueueQuery :: Query sndQueueQuery = [sql| SELECT - c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, + c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, q.snd_secure, q.snd_public_key, q.snd_private_key, q.e2e_pub_key, q.e2e_dh_secret, q.status, q.snd_queue_id, q.snd_primary, q.replace_snd_queue_id, q.switch_status, q.smp_client_version FROM snd_queues q @@ -2058,17 +2066,18 @@ sndQueueQuery = |] toSndQueue :: - (UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SenderId) + (UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SenderId, SenderCanSecure) :. (Maybe SndPublicAuthKey, SndPrivateAuthKey, Maybe C.PublicKeyX25519, C.DhSecretX25519, QueueStatus) :. (DBQueueId 'QSStored, Bool, Maybe Int64, Maybe SndSwitchStatus, VersionSMPC) -> SndQueue toSndQueue - ( (userId, keyHash, connId, host, port, sndId) - :. (sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret, status) + ( (userId, keyHash, connId, host, port, sndId, sndSecure) + :. (sndPubKey, sndPrivateKey@(C.APrivateAuthKey a pk), e2ePubKey, e2eDhSecret, status) :. (dbQueueId, primary, dbReplaceQueueId, sndSwchStatus, smpClientVersion) ) = let server = SMPServer host port keyHash - in SndQueue {userId, connId, server, sndId, sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret, status, dbQueueId, primary, dbReplaceQueueId, sndSwchStatus, smpClientVersion} + sndPublicKey = fromMaybe (C.APublicAuthKey a (C.publicKey pk)) sndPubKey + in SndQueue {userId, connId, server, sndId, sndSecure, sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret, status, dbQueueId, primary, dbReplaceQueueId, sndSwchStatus, smpClientVersion} getSndQueueById :: DB.Connection -> ConnId -> Int64 -> IO (Either StoreError SndQueue) getSndQueueById db connId dbSndId = @@ -2147,10 +2156,10 @@ insertRcvMsgDetails_ db connId RcvQueue {dbQueueId} RcvMsgData {msgMeta, interna ] DB.execute db "INSERT INTO encrypted_rcv_message_hashes (conn_id, hash) VALUES (?,?)" (connId, encryptedMsgHash) -updateHashRcv_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () -updateHashRcv_ dbConn connId RcvMsgData {msgMeta = MsgMeta {sndMsgId}, internalHash, internalRcvId} = +updateRcvMsgHash :: DB.Connection -> ConnId -> AgentMsgId -> InternalRcvId -> MsgHash -> IO () +updateRcvMsgHash db connId sndMsgId internalRcvId internalHash = DB.executeNamed - dbConn + db -- last_internal_rcv_msg_id equality check prevents race condition in case next id was reserved [sql| UPDATE connections @@ -2226,10 +2235,10 @@ insertSndMsgDetails_ dbConn connId SndMsgData {..} = ":previous_msg_hash" := prevMsgHash ] -updateHashSnd_ :: DB.Connection -> ConnId -> SndMsgData -> IO () -updateHashSnd_ dbConn connId SndMsgData {..} = +updateSndMsgHash :: DB.Connection -> ConnId -> InternalSndId -> MsgHash -> IO () +updateSndMsgHash db connId internalSndId internalHash = DB.executeNamed - dbConn + db -- last_internal_snd_msg_id equality check prevents race condition in case next id was reserved [sql| UPDATE connections diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index 340063f5c..1b8990ab8 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -73,6 +73,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wai import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats +import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Transport.Client (TransportHost) @@ -114,7 +115,8 @@ schemaMigrations = ("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery), ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem), ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays), - ("m20240518_servers_stats", m20240518_servers_stats, Just down_m20240518_servers_stats) + ("m20240518_servers_stats", m20240518_servers_stats, Just down_m20240518_servers_stats), + ("m20240624_snd_secure", m20240624_snd_secure, Just down_m20240624_snd_secure) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240624_snd_secure.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240624_snd_secure.hs new file mode 100644 index 000000000..7f82d4ecf --- /dev/null +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240624_snd_secure.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240624_snd_secure :: Query +m20240624_snd_secure = + [sql| +ALTER TABLE rcv_queues ADD COLUMN snd_secure INTEGER NOT NULL DEFAULT 0; +ALTER TABLE snd_queues ADD COLUMN snd_secure INTEGER NOT NULL DEFAULT 0; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'sender_key BLOB NOT NULL,', 'sender_key BLOB,') +WHERE name = 'conn_confirmations' AND type = 'table'; + +PRAGMA writable_schema=0; +|] + +down_m20240624_snd_secure :: Query +down_m20240624_snd_secure = + [sql| +ALTER TABLE rcv_queues DROP COLUMN snd_secure; +ALTER TABLE snd_queues DROP COLUMN snd_secure; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'sender_key BLOB,', 'sender_key BLOB NOT NULL,') +WHERE name = 'conn_confirmations' AND type = 'table'; + +PRAGMA writable_schema=0; +|] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index 50cf6d74a..80af08989 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -55,6 +55,7 @@ CREATE TABLE rcv_queues( server_key_hash BLOB, switch_status TEXT, deleted INTEGER NOT NULL DEFAULT 0, + snd_secure INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(host, port, rcv_id), FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE, @@ -77,6 +78,7 @@ CREATE TABLE snd_queues( replace_snd_queue_id INTEGER NULL, server_key_hash BLOB, switch_status TEXT, + snd_secure INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(host, port, snd_id), FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE @@ -132,7 +134,7 @@ CREATE TABLE conn_confirmations( confirmation_id BLOB NOT NULL PRIMARY KEY, conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, e2e_snd_pub_key BLOB NOT NULL, - sender_key BLOB NOT NULL, + sender_key BLOB, ratchet_state BLOB NOT NULL, sender_conn_info BLOB NOT NULL, accepted INTEGER NOT NULL, diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index de178e368..39cf32677 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -47,6 +47,7 @@ module Simplex.Messaging.Client subscribeSMPQueueNotifications, subscribeSMPQueuesNtfs, secureSMPQueue, + secureSndSMPQueue, enableSMPQueueNotifications, disableSMPQueueNotifications, enableSMPQueuesNtfs, @@ -655,9 +656,10 @@ createSMPQueue :: RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> + Bool -> ExceptT SMPClientError IO QueueIdsKeys -createSMPQueue c (rKey, rpKey) dhKey auth subMode = - sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode) >>= \case +createSMPQueue c (rKey, rpKey) dhKey auth subMode sndSecure = + sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode sndSecure) >>= \case IDS qik -> pure qik r -> throwE $ unexpectedResponse r @@ -729,6 +731,11 @@ secureSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> SndPublicAuth secureSMPQueue c rpKey rId senderKey = okSMPCommand (KEY senderKey) c rpKey rId {-# INLINE secureSMPQueue #-} +-- | Secure the SMP queue via sender queue ID. +secureSndSMPQueue :: SMPClient -> SndPrivateAuthKey -> SenderId -> SndPublicAuthKey -> ExceptT SMPClientError IO () +secureSndSMPQueue c spKey sId senderKey = okSMPCommand (SKEY senderKey) c spKey sId +{-# INLINE secureSndSMPQueue #-} + -- | Enable notifications for the queue for push notifications server. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#enable-notifications-command diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 5edea1719..d1b3f85d6 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -55,6 +55,7 @@ module Simplex.Messaging.Protocol ProtocolEncoding (..), Command (..), SubscriptionMode (..), + SenderCanSecure, Party (..), Cmd (..), DirectParty, @@ -133,6 +134,7 @@ module Simplex.Messaging.Protocol FwdTransmission (..), MsgFlags (..), initialSMPClientVersion, + currentSMPClientVersion, userProtocol, rcvMessageMeta, noMsgFlags, @@ -153,6 +155,7 @@ module Simplex.Messaging.Protocol legacyServerP, legacyStrEncodeServer, srvHostnamesSMPClientVersion, + sndAuthKeySMPClientVersion, sameSrvAddr, sameSrvAddr', noAuthSrv, @@ -240,8 +243,11 @@ initialSMPClientVersion = VersionSMPC 1 srvHostnamesSMPClientVersion :: VersionSMPC srvHostnamesSMPClientVersion = VersionSMPC 2 +sndAuthKeySMPClientVersion :: VersionSMPC +sndAuthKeySMPClientVersion = VersionSMPC 3 + currentSMPClientVersion :: VersionSMPC -currentSMPClientVersion = VersionSMPC 2 +currentSMPClientVersion = VersionSMPC 3 supportedSMPClientVRange :: VersionRangeSMPC supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClientVersion @@ -377,7 +383,7 @@ data Command (p :: Party) where -- 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 :: RcvPublicAuthKey -> RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> Command Recipient + NEW :: RcvPublicAuthKey -> RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> SenderCanSecure -> Command Recipient SUB :: Command Recipient KEY :: SndPublicAuthKey -> Command Recipient NKEY :: NtfPublicAuthKey -> RcvNtfPublicDhKey -> Command Recipient @@ -390,6 +396,7 @@ data Command (p :: Party) where 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 @@ -432,6 +439,8 @@ instance Encoding SubscriptionMode where 'C' -> pure SMOnlyCreate _ -> fail "bad SubscriptionMode" +type SenderCanSecure = Bool + newtype EncTransmission = EncTransmission ByteString deriving (Show) @@ -664,6 +673,7 @@ data CommandTag (p :: Party) where OFF_ :: CommandTag Recipient DEL_ :: CommandTag Recipient QUE_ :: CommandTag Recipient + SKEY_ :: CommandTag Sender SEND_ :: CommandTag Sender PING_ :: CommandTag Sender PRXY_ :: CommandTag ProxiedClient @@ -712,6 +722,7 @@ instance PartyI p => Encoding (CommandTag p) where OFF_ -> "OFF" DEL_ -> "DEL" QUE_ -> "QUE" + SKEY_ -> "SKEY" SEND_ -> "SEND" PING_ -> "PING" PRXY_ -> "PRXY" @@ -732,6 +743,7 @@ instance ProtocolMsgTag CmdTag where "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 SSender PING_ "PRXY" -> Just $ CT SProxiedClient PRXY_ @@ -1106,7 +1118,8 @@ instance FromJSON CorrId where data QueueIdsKeys = QIK { rcvId :: RecipientId, sndId :: SenderId, - rcvPublicDhKey :: RcvPublicDhKey + rcvPublicDhKey :: RcvPublicDhKey, + sndSecure :: SenderCanSecure } deriving (Eq, Show) @@ -1277,7 +1290,8 @@ class ProtocolMsgTag (Tag msg) => ProtocolEncoding v err msg | msg -> err, msg - instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where type Tag (Command p) = CommandTag p encodeProtocol v = \case - NEW rKey dhKey auth_ subMode + NEW rKey dhKey auth_ subMode sndSecure + | v >= sndAuthKeySMPVersion -> new <> e (auth_, subMode, sndSecure) | v >= subModeSMPVersion -> new <> auth <> e subMode | v == basicAuthSMPVersion -> new <> auth | otherwise -> new @@ -1293,6 +1307,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where 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_ @@ -1318,6 +1333,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where SEND {} | B.null entId -> Left $ CMD NO_ENTITY | otherwise -> Right cmd + SKEY _ + | isNothing auth || B.null entId -> Left $ CMD NO_AUTH + | otherwise -> Right cmd PING -> noAuthCmd PRXY {} -> noAuthCmd PFWD {} @@ -1344,9 +1362,10 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where CT SRecipient tag -> Cmd SRecipient <$> case tag of NEW_ - | v >= subModeSMPVersion -> new <*> auth <*> smpP - | v == basicAuthSMPVersion -> new <*> auth <*> pure SMSubscribe - | otherwise -> new <*> pure Nothing <*> pure SMSubscribe + | v >= sndAuthKeySMPVersion -> new <*> smpP <*> smpP <*> smpP + | v >= subModeSMPVersion -> new <*> auth <*> smpP <*> pure False + | v == basicAuthSMPVersion -> new <*> auth <*> pure SMSubscribe <*> pure False + | otherwise -> new <*> pure Nothing <*> pure SMSubscribe <*> pure False where new = NEW <$> _smpP <*> smpP auth = optional (A.char 'A' *> smpP) @@ -1361,6 +1380,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where QUE_ -> pure QUE CT SSender tag -> Cmd SSender <$> case tag of + SKEY_ -> SKEY <$> _smpP SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) PING_ -> pure PING RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP) @@ -1377,8 +1397,12 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where type Tag BrokerMsg = BrokerMsgTag - encodeProtocol _v = \case - IDS (QIK rcvId sndId srvDh) -> e (IDS_, ' ', rcvId, sndId, srvDh) + encodeProtocol v = \case + IDS (QIK rcvId sndId srvDh sndSecure) + | v >= sndAuthKeySMPVersion -> ids <> e sndSecure + | otherwise -> ids + where + ids = e (IDS_, ' ', rcvId, sndId, srvDh) MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} -> e (MSG_, ' ', msgId, Tail body) NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) @@ -1395,13 +1419,17 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where e :: Encoding a => a -> ByteString e = smpEncode - protocolP _v = \case + protocolP v = \case MSG_ -> do msgId <- _smpP MSG . RcvMessage msgId <$> bodyP where bodyP = EncRcvMsgBody . unTail <$> smpP - IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP) + IDS_ + | v >= sndAuthKeySMPVersion -> ids smpP + | otherwise -> ids $ pure False + where + ids p = IDS <$> (QIK <$> _smpP <*> smpP <*> smpP <*> p) NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP PKEY_ -> PKEY <$> _smpP <*> smpP <*> ((,) <$> C.certChainP <*> (C.getSignedExact <$> smpP)) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 5c8d13a5e..f673018bf 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -607,9 +607,10 @@ data VerificationResult = VRVerified (Maybe QueueRec) | VRFailed verifyTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult verifyTransmission auth_ tAuth authorized queueId cmd = case cmd of - Cmd SRecipient (NEW k _ _ _) -> pure $ Nothing `verifiedWith` k + Cmd SRecipient (NEW k _ _ _ _) -> pure $ Nothing `verifiedWith` k Cmd SRecipient _ -> verifyQueue (\q -> Just q `verifiedWith` recipientKey q) <$> get SRecipient - -- SEND will be accepted without authorization before the queue is secured with KEY command + -- SEND will be accepted without authorization before the queue is secured with KEY or SKEY command + Cmd SSender (SKEY k) -> verifyQueue (\q -> Just q `verifiedWith` k) <$> get SSender Cmd SSender SEND {} -> verifyQueue (\q -> Just q `verified` maybe (isNothing tAuth) verify (senderKey q)) <$> get SSender Cmd SSender PING -> pure $ VRVerified Nothing Cmd SSender RFWD {} -> pure $ VRVerified Nothing @@ -768,13 +769,18 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi transportErr :: TransportError -> ErrorType transportErr = PROXY . BROKER . TRANSPORT mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> TVar Int) -> m () - mkIncProxyStats ps psOwn = \own sel -> do + mkIncProxyStats ps psOwn own sel = do atomically $ modifyTVar' (sel ps) (+ 1) when own $ atomically $ modifyTVar' (sel psOwn) (+ 1) processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Maybe (Transmission BrokerMsg)) processCommand (qr_, (corrId, queueId, cmd)) = case cmd of Cmd SProxiedClient command -> processProxiedCmd (corrId, queueId, command) Cmd SSender command -> Just <$> case command of + SKEY sKey -> (corrId,queueId,) <$> case qr_ of + Just QueueRec {sndSecure, recipientId} + | sndSecure -> secureQueue_ "SKEY" recipientId sKey + | otherwise -> pure $ ERR AUTH + Nothing -> pure $ ERR INTERNAL SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody PING -> pure (corrId, "", PONG) RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock @@ -782,10 +788,10 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Cmd SRecipient command -> do st <- asks queueStore Just <$> case command of - NEW rKey dhKey auth subMode -> + NEW rKey dhKey auth subMode sndSecure -> ifM allowNew - (createQueue st rKey dhKey subMode) + (createQueue st rKey dhKey subMode sndSecure) (pure (corrId, queueId, ERR AUTH)) where allowNew = do @@ -794,18 +800,20 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi SUB -> withQueue (`subscribeQueue` queueId) GET -> withQueue getMessage ACK msgId -> withQueue (`acknowledgeMsg` msgId) - KEY sKey -> secureQueue_ st sKey + KEY sKey -> (corrId,queueId,) <$> case qr_ of + Just QueueRec {recipientId} -> secureQueue_ "KEY" recipientId sKey + Nothing -> pure $ ERR INTERNAL NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey NDEL -> deleteQueueNotifier_ st OFF -> suspendQueue_ st DEL -> delQueueAndMsgs st QUE -> withQueue getQueueInfo where - createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> M (Transmission BrokerMsg) - createQueue st recipientKey dhKey subMode = time "NEW" $ do + createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> SenderCanSecure -> M (Transmission BrokerMsg) + createQueue st recipientKey dhKey subMode sndSecure = time "NEW" $ do (rcvPublicDhKey, privDhKey) <- atomically . C.generateKeyPair =<< asks random let rcvDhSecret = C.dh' dhKey privDhKey - qik (rcvId, sndId) = QIK {rcvId, sndId, rcvPublicDhKey} + qik (rcvId, sndId) = QIK {rcvId, sndId, rcvPublicDhKey, sndSecure} qRec (recipientId, senderId) = QueueRec { recipientId, @@ -814,7 +822,8 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi rcvDhSecret, senderKey = Nothing, notifier = Nothing, - status = QueueActive + status = QueueActive, + sndSecure } (corrId,queueId,) <$> addQueueRetry 3 qik qRec where @@ -849,12 +858,13 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi n <- asks $ queueIdBytes . config liftM2 (,) (randomId n) (randomId n) - secureQueue_ :: QueueStore -> SndPublicAuthKey -> M (Transmission BrokerMsg) - secureQueue_ st sKey = time "KEY" $ do - withLog $ \s -> logSecureQueue s queueId sKey + secureQueue_ :: T.Text -> RecipientId -> SndPublicAuthKey -> M BrokerMsg + secureQueue_ name rId sKey = time name $ do + withLog $ \s -> logSecureQueue s rId sKey + st <- asks queueStore stats <- asks serverStats atomically $ modifyTVar' (qSecured stats) (+ 1) - atomically $ (corrId,queueId,) . either ERR (const OK) <$> secureQueue st queueId sKey + atomically $ either ERR (const OK) <$> secureQueue st rId sKey addQueueNotifier_ :: QueueStore -> NtfPublicAuthKey -> RcvNtfPublicDhKey -> M (Transmission BrokerMsg) addQueueNotifier_ st notifierKey dhKey = time "NKEY" $ do diff --git a/src/Simplex/Messaging/Server/QueueStore.hs b/src/Simplex/Messaging/Server/QueueStore.hs index cd1b94215..8d5bd8fff 100644 --- a/src/Simplex/Messaging/Server/QueueStore.hs +++ b/src/Simplex/Messaging/Server/QueueStore.hs @@ -14,6 +14,7 @@ data QueueRec = QueueRec rcvDhSecret :: !RcvDhSecret, senderId :: !SenderId, senderKey :: !(Maybe SndPublicAuthKey), + sndSecure :: !SenderCanSecure, notifier :: !(Maybe NtfCreds), status :: !ServerQueueStatus } diff --git a/src/Simplex/Messaging/Server/StoreLog.hs b/src/Simplex/Messaging/Server/StoreLog.hs index b1011c404..d1ce15ed6 100644 --- a/src/Simplex/Messaging/Server/StoreLog.hs +++ b/src/Simplex/Messaging/Server/StoreLog.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} @@ -53,7 +54,7 @@ data StoreLogRecord | DeleteNotifier QueueId instance StrEncoding QueueRec where - strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier} = + strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier} = B.unwords [ "rid=" <> strEncode recipientId, "rk=" <> strEncode recipientKey, @@ -61,6 +62,7 @@ instance StrEncoding QueueRec where "sid=" <> strEncode senderId, "sk=" <> strEncode senderKey ] + <> if sndSecure then " sndSecure=" <> strEncode sndSecure else "" <> maybe "" notifierStr notifier where notifierStr ntfCreds = " notifier=" <> strEncode ntfCreds @@ -71,8 +73,9 @@ instance StrEncoding QueueRec where rcvDhSecret <- "rdh=" *> strP_ senderId <- "sid=" *> strP_ senderKey <- "sk=" *> strP + sndSecure <- (" sndSecure=" *> strP) <|> pure False notifier <- optional $ " notifier=" *> strP - pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier, status = QueueActive} + pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier, status = QueueActive} instance StrEncoding StoreLogRecord where strEncode = \case diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 7088480f5..d7f81f563 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -46,6 +46,7 @@ module Simplex.Messaging.Transport subModeSMPVersion, authCmdsSMPVersion, sendingProxySMPVersion, + sndAuthKeySMPVersion, simplexMQVersion, smpBlockSize, TransportConfig (..), @@ -156,14 +157,17 @@ authCmdsSMPVersion = VersionSMP 7 sendingProxySMPVersion :: VersionSMP sendingProxySMPVersion = VersionSMP 8 +sndAuthKeySMPVersion :: VersionSMP +sndAuthKeySMPVersion = VersionSMP 9 + currentClientSMPRelayVersion :: VersionSMP -currentClientSMPRelayVersion = VersionSMP 8 +currentClientSMPRelayVersion = VersionSMP 9 legacyServerSMPRelayVersion :: VersionSMP legacyServerSMPRelayVersion = VersionSMP 6 currentServerSMPRelayVersion :: VersionSMP -currentServerSMPRelayVersion = VersionSMP 8 +currentServerSMPRelayVersion = VersionSMP 9 -- Max SMP protocol version to be used in e2e encrypted -- connection between client and server, as defined by SMP proxy. @@ -171,7 +175,7 @@ currentServerSMPRelayVersion = VersionSMP 8 -- to prevent client version fingerprinting by the -- destination relays when clients upgrade at different times. proxiedSMPRelayVersion :: VersionSMP -proxiedSMPRelayVersion = VersionSMP 8 +proxiedSMPRelayVersion = VersionSMP 9 -- minimal supported protocol version is 4 -- TODO remove code that supports sending commands without batching diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs index 20480f84c..8684c787c 100644 --- a/tests/AgentTests/ConnectionRequestTests.hs +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -7,7 +7,12 @@ {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} -module AgentTests.ConnectionRequestTests where +module AgentTests.ConnectionRequestTests + ( connectionRequestTests, + connReqData, + queueAddr, + testE2ERatchetParams12, + ) where import Data.ByteString (ByteString) import Network.HTTP.Types (urlEncode) @@ -15,179 +20,228 @@ import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (ProtocolServer (..), pattern VersionSMPC, supportedSMPClientVRange) +import Simplex.Messaging.Protocol (ProtocolServer (..), currentSMPClientVersion, supportedSMPClientVRange, pattern VersionSMPC) import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import Simplex.Messaging.Version import Test.Hspec -uri :: String -uri = "smp.simplex.im" - srv :: SMPServer -srv = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251") +srv = SMPServer "smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion" "5223" (C.KeyHash "\215m\248\251") + +srv1 :: SMPServer +srv1 = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251") queueAddr :: SMPQueueAddress queueAddr = SMPQueueAddress { smpServer = srv, senderId = "\223\142z\251", - dhPublicKey = testDhKey + dhPublicKey = testDhKey, + sndSecure = False } +queueAddrSK :: SMPQueueAddress +queueAddrSK = queueAddr {sndSecure = True} + +queueAddr1 :: SMPQueueAddress +queueAddr1 = queueAddr {smpServer = srv1} + queueAddrNoPort :: SMPQueueAddress queueAddrNoPort = queueAddr {smpServer = srv {port = ""}} +queueAddrNoPort1 :: SMPQueueAddress +queueAddrNoPort1 = queueAddr {smpServer = srv1 {port = ""}} + +-- current version range includes version 1 and it uses legacy encoding queue :: SMPQueueUri queue = SMPQueueUri supportedSMPClientVRange queueAddr +queueSK :: SMPQueueUri +queueSK = SMPQueueUri supportedSMPClientVRange queueAddrSK + +queueStr :: ByteString +queueStr = "smp://1234-w==@smp.simplex.im:5223/3456-w==#/?v=1-3&dh=" <> url testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion" + +queueStrSK :: ByteString +queueStrSK = "smp://1234-w==@smp.simplex.im:5223/3456-w==#/?v=1-3&dh=" <> url testDhKeyStr <> "&k=s" <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion" + +queue1 :: SMPQueueUri +queue1 = SMPQueueUri supportedSMPClientVRange queueAddr1 + +queue1Str :: ByteString +queue1Str = "smp://1234-w==@smp.simplex.im:5223/3456-w==#/?v=1-3&dh=" <> url testDhKeyStr + queueV1 :: SMPQueueUri queueV1 = SMPQueueUri (mkVersionRange (VersionSMPC 1) (VersionSMPC 1)) queueAddr +queueV1NoPort :: SMPQueueUri +queueV1NoPort = (queueV1 :: SMPQueueUri) {queueAddress = queueAddrNoPort} + +-- version range 2-3 uses new encoding +-- it is fixed/changed in v5.8.2. +queueNew :: SMPQueueUri +queueNew = SMPQueueUri (mkVersionRange (VersionSMPC 2) currentSMPClientVersion) queueAddr + +queueNewStr :: ByteString +queueNewStr = "smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion:5223/3456-w==#/?v=2-3&dh=" <> url testDhKeyStr + +queueNewStr' :: ByteString +queueNewStr' = "smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion:5223/3456-w==#/?v=2-3&dh=" <> testDhKeyStr + +queueNewNoPort :: SMPQueueUri +queueNewNoPort = (queueNew :: SMPQueueUri) {queueAddress = queueAddrNoPort} + +queueNew1 :: SMPQueueUri +queueNew1 = SMPQueueUri (mkVersionRange (VersionSMPC 2) currentSMPClientVersion) queueAddr1 + +queueNew1Str :: ByteString +queueNew1Str = "smp://1234-w==@smp.simplex.im:5223/3456-w==#/?v=2-3&dh=" <> url testDhKeyStr + +queueNew1NoPort :: SMPQueueUri +queueNew1NoPort = (queueNew1 :: SMPQueueUri) {queueAddress = queueAddrNoPort1} + testDhKey :: C.PublicKeyX25519 testDhKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" testDhKeyStr :: ByteString testDhKeyStr = strEncode testDhKey -testDhKeyStrUri :: ByteString -testDhKeyStrUri = urlEncode True testDhKeyStr - connReqData :: ConnReqUriData connReqData = ConnReqUriData { crScheme = SSSimplex, - crAgentVRange = mkVersionRange (VersionSMPA 2) (VersionSMPA 2), - crSmpQueues = [queueV1], + crAgentVRange = supportedSMPAgentVRange, + crSmpQueues = [queue], crClientData = Nothing } +connReqDataSK :: ConnReqUriData +connReqDataSK = connReqData {crSmpQueues = [queueSK]} + +connReqData1 :: ConnReqUriData +connReqData1 = connReqData {crSmpQueues = [queue1]} + +connReqDataV1 :: ConnReqUriData +connReqDataV1 = connReqData {crAgentVRange = mkVersionRange (VersionSMPA 1) (VersionSMPA 1)} + +connReqDataV2 :: ConnReqUriData +connReqDataV2 = connReqData {crAgentVRange = mkVersionRange (VersionSMPA 2) (VersionSMPA 2)} + +connReqDataNew :: ConnReqUriData +connReqDataNew = connReqData {crSmpQueues = [queueNew]} + +connReqDataNew1 :: ConnReqUriData +connReqDataNew1 = connReqData {crSmpQueues = [queueNew1]} + testDhPubKey :: C.PublicKeyX448 testDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U=" testE2ERatchetParams :: RcvE2ERatchetParamsUri 'C.X448 testE2ERatchetParams = E2ERatchetParamsUri (mkVersionRange (VersionE2E 1) (VersionE2E 1)) testDhPubKey testDhPubKey Nothing +testE2ERatchetParamsStrUri :: ByteString +testE2ERatchetParamsStrUri = "v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + testE2ERatchetParams12 :: RcvE2ERatchetParamsUri 'C.X448 testE2ERatchetParams12 = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey Nothing connectionRequest :: AConnectionRequestUri -connectionRequest = - ACR SCMInvitation $ - CRInvitationUri connReqData testE2ERatchetParams +connectionRequest = ACR SCMInvitation $ CRInvitationUri connReqData testE2ERatchetParams + +connectionRequestSK :: AConnectionRequestUri +connectionRequestSK = ACR SCMInvitation $ CRInvitationUri connReqDataSK testE2ERatchetParams + +connectionRequestV1 :: AConnectionRequestUri +connectionRequestV1 = ACR SCMInvitation $ CRInvitationUri connReqDataV1 testE2ERatchetParams + +connectionRequest1 :: AConnectionRequestUri +connectionRequest1 = ACR SCMInvitation $ CRInvitationUri connReqData1 testE2ERatchetParams + +connectionRequestNew :: AConnectionRequestUri +connectionRequestNew = ACR SCMInvitation $ CRInvitationUri connReqDataNew testE2ERatchetParams + +connectionRequestNew1 :: AConnectionRequestUri +connectionRequestNew1 = ACR SCMInvitation $ CRInvitationUri connReqDataNew1 testE2ERatchetParams contactAddress :: AConnectionRequestUri contactAddress = ACR SCMContact $ CRContactUri connReqData -connectionRequestCurrentRange :: AConnectionRequestUri -connectionRequestCurrentRange = - ACR SCMInvitation $ - CRInvitationUri - connReqData {crAgentVRange = supportedSMPAgentVRange, crSmpQueues = [queueV1, queueV1]} - testE2ERatchetParams12 +contactAddressV2 :: AConnectionRequestUri +contactAddressV2 = ACR SCMContact $ CRContactUri connReqDataV2 + +contactAddressNew :: AConnectionRequestUri +contactAddressNew = ACR SCMContact $ CRContactUri connReqDataNew + +connectionRequest2queues :: AConnectionRequestUri +connectionRequest2queues = ACR SCMInvitation $ CRInvitationUri connReqData {crSmpQueues = [queue, queue]} testE2ERatchetParams + +connectionRequest2queuesNew :: AConnectionRequestUri +connectionRequest2queuesNew = ACR SCMInvitation $ CRInvitationUri connReqDataNew {crSmpQueues = [queueNew, queueNew]} testE2ERatchetParams + +contactAddress2queues :: AConnectionRequestUri +contactAddress2queues = ACR SCMContact $ CRContactUri connReqData {crSmpQueues = [queue, queue]} + +contactAddress2queuesNew :: AConnectionRequestUri +contactAddress2queuesNew = ACR SCMContact $ CRContactUri connReqDataNew {crSmpQueues = [queueNew, queueNew]} connectionRequestClientDataEmpty :: AConnectionRequestUri -connectionRequestClientDataEmpty = - ACR SCMInvitation $ - CRInvitationUri connReqData {crClientData = Just "{}"} testE2ERatchetParams +connectionRequestClientDataEmpty = ACR SCMInvitation $ CRInvitationUri connReqData {crClientData = Just "{}"} testE2ERatchetParams -connectionRequestClientData :: AConnectionRequestUri -connectionRequestClientData = - ACR SCMInvitation $ - CRInvitationUri connReqData {crClientData = Just "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}"} testE2ERatchetParams +contactAddressClientData :: AConnectionRequestUri +contactAddressClientData = ACR SCMContact $ CRContactUri connReqData {crClientData = Just "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}"} + +url :: ByteString -> ByteString +url = urlEncode True + +(==#) :: (StrEncoding a, HasCallStack) => a -> ByteString -> Expectation +a ==# s = strEncode a `shouldBe` s + +(#==) :: (StrEncoding a, Eq a, Show a, HasCallStack) => a -> ByteString -> Expectation +a #== s = strDecode s `shouldBe` Right a + +(#==#) :: (StrEncoding a, Eq a, Show a, HasCallStack) => a -> ByteString -> Expectation +a #==# s = do + a ==# s + a #== s connectionRequestTests :: Spec connectionRequestTests = describe "connection request parsing / serializing" $ do - it "should serialize SMP queue URIs" $ do - strEncode (queue :: SMPQueueUri) {queueAddress = queueAddrNoPort} - `shouldBe` "smp://1234-w==@smp.simplex.im/3456-w==#/?v=1-2&dh=" <> testDhKeyStrUri - strEncode queue {clientVRange = mkVersionRange (VersionSMPC 1) (VersionSMPC 2)} - `shouldBe` "smp://1234-w==@smp.simplex.im:5223/3456-w==#/?v=1-2&dh=" <> testDhKeyStrUri - it "should parse SMP queue URIs" $ do - strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1-2&dh=" <> testDhKeyStr) - `shouldBe` Right (queue :: SMPQueueUri) {queueAddress = queueAddrNoPort} - strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr) - `shouldBe` Right (queueV1 :: SMPQueueUri) {queueAddress = queueAddrNoPort} - strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr) - `shouldBe` Right queueV1 - strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1-2&extra_param=abc") - `shouldBe` Right queue - strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#/?extra_param=abc&v=1&dh=" <> testDhKeyStr) - `shouldBe` Right queueV1 - strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1&extra_param=abc") - `shouldBe` Right queueV1 - it "should serialize connection requests" $ do - strEncode connectionRequest - `shouldBe` "simplex:/invitation#/?v=2&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> urlEncode True testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - strEncode connectionRequestCurrentRange - `shouldBe` "simplex:/invitation#/?v=2-5&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> urlEncode True testDhKeyStrUri - <> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> urlEncode True testDhKeyStrUri - <> "&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - strEncode connectionRequestClientDataEmpty - `shouldBe` "simplex:/invitation#/?v=2&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> urlEncode True testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&data=%7B%7D" - strEncode connectionRequestClientData - `shouldBe` "simplex:/invitation#/?v=2&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> urlEncode True testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&data=%7B%22type%22%3A%22group_link%22%2C%20%22group_link_id%22%3A%22abc%22%7D" - it "should parse connection requests" $ do - strDecode - ( "https://simplex.chat/contact#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" - <> testDhKeyStrUri - <> "&v=1" -- adjusted to v2 - ) - `shouldBe` Right contactAddress - strDecode - ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" - <> testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&v=2" - ) - `shouldBe` Right connectionRequest - strDecode - ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&v=2" - ) - `shouldBe` Right connectionRequest - strDecode - ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> testDhKeyStrUri - <> "&e2e=v%3D1-1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&v=2-2" - ) - `shouldBe` Right connectionRequest - strDecode - ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26extra_param%3Dabc%26dh%3D" - <> testDhKeyStrUri - <> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> testDhKeyStrUri - <> "&e2e=extra_key%3Dnew%26v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&some_new_param=abc" - <> "&v=2-5" - ) - `shouldBe` Right connectionRequestCurrentRange - strDecode - ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&data=%7B%7D" - <> "&v=2-2" - ) - `shouldBe` Right connectionRequestClientDataEmpty - strDecode - ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" - <> testDhKeyStrUri - <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> "&data=%7B%22type%22%3A%22group_link%22%2C%20%22group_link_id%22%3A%22abc%22%7D" - <> "&v=2" - ) - `shouldBe` Right connectionRequestClientData + it "should serialize and parse SMP queue URIs" $ do + queue #==# queueStr + queue #== ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1-3&extra_param=abc&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") + queueSK #==# queueStrSK + queue1 #==# queue1Str + queueNew #==# queueNewStr + queueNew #== queueNewStr' + queueNew1 #==# queueNew1Str + queueNewNoPort #==# ("smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion/3456-w==#/?v=2-3&dh=" <> url testDhKeyStr) + queueNew1NoPort #==# ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=2-3&dh=" <> url testDhKeyStr) + queueV1 #==# ("smp://1234-w==@smp.simplex.im:5223/3456-w==#/?v=1&dh=" <> url testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") + queueV1 #== ("smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion:5223/3456-w==#" <> testDhKeyStr) + queueV1 #== ("smp://1234-w==@smp.simplex.im:5223/3456-w==#/?extra_param=abc&v=1&dh=" <> testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") + queueV1 #== ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1&extra_param=abc&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") + queueV1NoPort #==# ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1&dh=" <> url testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") + queueV1NoPort #== ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1-1&dh=" <> url testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") + queueV1NoPort #== ("smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion/3456-w==#" <> testDhKeyStr) + it "should serialize and parse connection invitations and contact addresses" $ do + connectionRequest #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest #== ("https://simplex.chat/invitation#/?v=2-6&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestSK #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueStrSK <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest1 #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queue1Str <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest2queues #==# ("simplex:/invitation#/?v=2-6&smp=" <> url (queueStr <> ";" <> queueStr) <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestNew #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueNewStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestNew1 #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueNew1Str <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest2queuesNew #==# ("simplex:/invitation#/?v=2-6&smp=" <> url (queueNewStr <> ";" <> queueNewStr) <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestV1 #== ("https://simplex.chat/invitation#/?v=1&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestClientDataEmpty #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&data=" <> url "{}") + contactAddress #==# ("simplex:/contact#/?v=2-6&smp=" <> url queueStr) + contactAddress #== ("https://simplex.chat/contact#/?v=2-6&smp=" <> url queueStr) + contactAddress2queues #==# ("simplex:/contact#/?v=2-6&smp=" <> url (queueStr <> ";" <> queueStr)) + contactAddressNew #==# ("simplex:/contact#/?v=2-6&smp=" <> url queueNewStr) + contactAddress2queuesNew #==# ("simplex:/contact#/?v=2-6&smp=" <> url (queueNewStr <> ";" <> queueNewStr)) + contactAddressV2 #==# ("simplex:/contact#/?v=2&smp=" <> url queueStr) + contactAddressV2 #== ("https://simplex.chat/contact#/?v=1&smp=" <> url queueStr) -- adjusted to v2 + contactAddressV2 #== ("https://simplex.chat/contact#/?v=1-2&smp=" <> url queueStr) -- adjusted to v2 + contactAddressV2 #== ("https://simplex.chat/contact#/?v=2-2&smp=" <> url queueStr) + contactAddressClientData #==# ("simplex:/contact#/?v=2-6&smp=" <> url queueStr <> "&data=" <> url "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}") diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index d52c12877..9f133ebf5 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -25,7 +25,7 @@ module AgentTests.FunctionalAPITests withAgentClients2, withAgentClients3, makeConnection, - exchangeGreetingsMsgId, + exchangeGreetings, switchComplete, createConnection, joinConnection, @@ -47,7 +47,7 @@ module AgentTests.FunctionalAPITests pattern Msg, pattern Msg', pattern SENT, - agentCfgV7, + agentCfgVPrevPQ, ) where @@ -75,7 +75,7 @@ import Data.Word (Word16) import qualified Database.SQLite.Simple as SQL import GHC.Stack (withFrozenCallStack) import SMPAgentClient -import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) +import SMPClient (cfg, prevRange, prevVersion, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import qualified Simplex.Messaging.Agent as A import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork) @@ -89,13 +89,13 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Transport (NTFVersion, authBatchCmdsNTFVersion, pattern VersionNTF) +import Simplex.Messaging.Notifications.Transport (NTFVersion, pattern VersionNTF) import Simplex.Messaging.Protocol (BasicAuth, ErrorType (..), MsgBody, ProtocolServer (..), SubscriptionMode (..), supportedSMPClientVRange) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.QueueStore.QueueInfo -import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion, supportedSMPHandshakes) +import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion, sndAuthKeySMPVersion, supportedSMPHandshakes) import Simplex.Messaging.Util (bshow, diffToMicroseconds) import Simplex.Messaging.Version (VersionRange (..)) import qualified Simplex.Messaging.Version as V @@ -195,46 +195,25 @@ pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMs smpCfgVPrev :: ProtocolClientConfig SMPVersion smpCfgVPrev = (smpCfg agentCfg) {clientALPN = Nothing, serverVRange = prevRange $ serverVRange $ smpCfg agentCfg} -smpCfgV7 :: ProtocolClientConfig SMPVersion -smpCfgV7 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} - ntfCfgVPrev :: ProtocolClientConfig NTFVersion ntfCfgVPrev = (ntfCfg agentCfg) {clientALPN = Nothing, serverVRange = V.mkVersionRange (VersionNTF 1) (VersionNTF 1)} -ntfCfgV2 :: ProtocolClientConfig NTFVersion -ntfCfgV2 = (ntfCfg agentCfg) {serverVRange = V.mkVersionRange (VersionNTF 1) authBatchCmdsNTFVersion} - agentCfgVPrev :: AgentConfig -agentCfgVPrev = +agentCfgVPrev = agentCfgVPrevPQ {e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg} + +agentCfgVPrevPQ :: AgentConfig +agentCfgVPrevPQ = agentCfg { sndAuthAlg = C.AuthAlg C.SEd25519, smpAgentVRange = prevRange $ smpAgentVRange agentCfg, smpClientVRange = prevRange $ smpClientVRange agentCfg, - e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg, smpCfg = smpCfgVPrev, ntfCfg = ntfCfgVPrev } --- agent config for the next client version -agentCfgV7 :: AgentConfig -agentCfgV7 = - agentCfg - { sndAuthAlg = C.AuthAlg C.SX25519, - smpAgentVRange = V.mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, - e2eEncryptVRange = V.mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, - smpCfg = smpCfgV7, - ntfCfg = ntfCfgV2 - } - agentCfgRatchetVPrev :: AgentConfig agentCfgRatchetVPrev = agentCfg {e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg} -prevRange :: VersionRange v -> VersionRange v -prevRange vr = vr {maxVersion = max (minVersion vr) (prevVersion $ maxVersion vr)} - -prevVersion :: Version v -> Version v -prevVersion (Version v) = Version (v - 1) - mkVersionRange :: Word16 -> Word16 -> VersionRange v mkVersionRange v1 v2 = V.mkVersionRange (Version v1) (Version v2) @@ -345,6 +324,8 @@ functionalAPITests t = do it "should synchronize ratchets when clients start synchronization simultaneously" $ testRatchetSyncSimultaneous t describe "Subscription mode OnlyCreate" $ do + it "messages delivered only when polled (v8 - slow handshake)" $ + withSmpServer t testOnlyCreatePullSlowHandshake it "messages delivered only when polled" $ withSmpServer t testOnlyCreatePull describe "Inactive client disconnection" $ do @@ -371,14 +352,16 @@ functionalAPITests t = do withSmpServer t $ testBatchedPendingMessages 10 5 describe "Async agent commands" $ do - it "should connect using async agent commands" $ - withSmpServer t testAsyncCommands + describe "connect using async agent commands" $ + testBasicMatrix2 t testAsyncCommands it "should restore and complete async commands on restart" $ testAsyncCommandsRestore t - it "should accept connection using async command" $ - withSmpServer t testAcceptContactAsync + describe "accept connection using async command" $ + testBasicMatrix2 t testAcceptContactAsync it "should delete connections using async command when server connection fails" $ testDeleteConnectionAsync t + it "join connection when reply queue creation fails (v8 - slow handshake)" $ + testJoinConnectionAsyncReplyErrorV8 t it "join connection when reply queue creation fails" $ testJoinConnectionAsyncReplyError t describe "delete connection waiting for delivery" $ do @@ -421,29 +404,30 @@ functionalAPITests t = do describe "SMP basic auth" $ do let v4 = prevVersion basicAuthSMPVersion forM_ (nub [prevVersion authCmdsSMPVersion, authCmdsSMPVersion, currentServerSMPRelayVersion]) $ \v -> do + let baseId = if v >= sndAuthKeySMPVersion then 1 else 3 describe ("v" <> show v <> ": with server auth") $ do -- allow NEW | server auth, v | clnt1 auth, v | clnt2 auth, v | 2 - success, 1 - JOIN fail, 0 - NEW fail - it "success " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Just "abcd", v) `shouldReturn` 2 - it "disabled " $ testBasicAuth t False (Just "abcd", v) (Just "abcd", v) (Just "abcd", v) `shouldReturn` 0 - it "NEW fail, no auth " $ testBasicAuth t True (Just "abcd", v) (Nothing, v) (Just "abcd", v) `shouldReturn` 0 - it "NEW fail, bad auth " $ testBasicAuth t True (Just "abcd", v) (Just "wrong", v) (Just "abcd", v) `shouldReturn` 0 - it "NEW fail, version " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v4) (Just "abcd", v) `shouldReturn` 0 - it "JOIN fail, no auth " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Nothing, v) `shouldReturn` 1 - it "JOIN fail, bad auth " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Just "wrong", v) `shouldReturn` 1 - it "JOIN fail, version " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Just "abcd", v4) `shouldReturn` 1 + it "success " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Just "abcd", v) baseId `shouldReturn` 2 + it "disabled " $ testBasicAuth t False (Just "abcd", v) (Just "abcd", v) (Just "abcd", v) baseId `shouldReturn` 0 + it "NEW fail, no auth " $ testBasicAuth t True (Just "abcd", v) (Nothing, v) (Just "abcd", v) baseId `shouldReturn` 0 + it "NEW fail, bad auth " $ testBasicAuth t True (Just "abcd", v) (Just "wrong", v) (Just "abcd", v) baseId `shouldReturn` 0 + it "NEW fail, version " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v4) (Just "abcd", v) baseId `shouldReturn` 0 + it "JOIN fail, no auth " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Nothing, v) baseId `shouldReturn` 1 + it "JOIN fail, bad auth " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Just "wrong", v) baseId `shouldReturn` 1 + it "JOIN fail, version " $ testBasicAuth t True (Just "abcd", v) (Just "abcd", v) (Just "abcd", v4) baseId `shouldReturn` 1 describe ("v" <> show v <> ": no server auth") $ do - it "success " $ testBasicAuth t True (Nothing, v) (Nothing, v) (Nothing, v) `shouldReturn` 2 - it "srv disabled" $ testBasicAuth t False (Nothing, v) (Nothing, v) (Nothing, v) `shouldReturn` 0 - it "version srv " $ testBasicAuth t True (Nothing, v4) (Nothing, v) (Nothing, v) `shouldReturn` 2 - it "version fst " $ testBasicAuth t True (Nothing, v) (Nothing, v4) (Nothing, v) `shouldReturn` 2 - it "version snd " $ testBasicAuth t True (Nothing, v) (Nothing, v) (Nothing, v4) `shouldReturn` 2 - it "version both" $ testBasicAuth t True (Nothing, v) (Nothing, v4) (Nothing, v4) `shouldReturn` 2 - it "version all " $ testBasicAuth t True (Nothing, v4) (Nothing, v4) (Nothing, v4) `shouldReturn` 2 - it "auth fst " $ testBasicAuth t True (Nothing, v) (Just "abcd", v) (Nothing, v) `shouldReturn` 2 - it "auth fst 2 " $ testBasicAuth t True (Nothing, v4) (Just "abcd", v) (Nothing, v) `shouldReturn` 2 - it "auth snd " $ testBasicAuth t True (Nothing, v) (Nothing, v) (Just "abcd", v) `shouldReturn` 2 - it "auth both " $ testBasicAuth t True (Nothing, v) (Just "abcd", v) (Just "abcd", v) `shouldReturn` 2 - it "auth, disabled" $ testBasicAuth t False (Nothing, v) (Just "abcd", v) (Just "abcd", v) `shouldReturn` 0 + it "success " $ testBasicAuth t True (Nothing, v) (Nothing, v) (Nothing, v) baseId `shouldReturn` 2 + it "srv disabled" $ testBasicAuth t False (Nothing, v) (Nothing, v) (Nothing, v) baseId `shouldReturn` 0 + it "version srv " $ testBasicAuth t True (Nothing, v4) (Nothing, v) (Nothing, v) 3 `shouldReturn` 2 + it "version fst " $ testBasicAuth t True (Nothing, v) (Nothing, v4) (Nothing, v) baseId `shouldReturn` 2 + it "version snd " $ testBasicAuth t True (Nothing, v) (Nothing, v) (Nothing, v4) 3 `shouldReturn` 2 + it "version both" $ testBasicAuth t True (Nothing, v) (Nothing, v4) (Nothing, v4) 3 `shouldReturn` 2 + it "version all " $ testBasicAuth t True (Nothing, v4) (Nothing, v4) (Nothing, v4) 3 `shouldReturn` 2 + it "auth fst " $ testBasicAuth t True (Nothing, v) (Just "abcd", v) (Nothing, v) baseId `shouldReturn` 2 + it "auth fst 2 " $ testBasicAuth t True (Nothing, v4) (Just "abcd", v) (Nothing, v) 3 `shouldReturn` 2 + it "auth snd " $ testBasicAuth t True (Nothing, v) (Nothing, v) (Just "abcd", v) baseId `shouldReturn` 2 + it "auth both " $ testBasicAuth t True (Nothing, v) (Just "abcd", v) (Just "abcd", v) baseId `shouldReturn` 2 + it "auth, disabled" $ testBasicAuth t False (Nothing, v) (Just "abcd", v) (Just "abcd", v) baseId `shouldReturn` 0 describe "SMP server test via agent API" $ do it "should pass without basic auth" $ testSMPServerConnectionTest t Nothing (noAuthSrv testSMPServer2) `shouldReturn` Nothing let srv1 = testSMPServer2 {keyHash = "1234"} @@ -471,8 +455,8 @@ functionalAPITests t = do it "server should respond with queue and subscription information" $ withSmpServer t testServerQueueInfo -testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int -testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do +testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> AgentMsgId -> IO Int +testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 baseId = do let testCfg = cfg {allowNewQueues, newQueueBasicAuth = srvAuth, smpServerVRange = V.mkVersionRange batchCmdsSMPVersion srvVersion} canCreate1 = canCreateQueue allowNewQueues srv clnt1 canCreate2 = canCreateQueue allowNewQueues srv clnt2 @@ -480,7 +464,7 @@ testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do | canCreate1 && canCreate2 = 2 | canCreate1 = 1 | otherwise = 0 - created <- withSmpServerConfigOn t testCfg testPort $ \_ -> testCreateQueueAuth srvVersion clnt1 clnt2 + created <- withSmpServerConfigOn t testCfg testPort $ \_ -> testCreateQueueAuth srvVersion clnt1 clnt2 baseId created `shouldBe` expected pure created @@ -491,26 +475,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = testMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True - it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False - it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False - it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False - it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False +testBasicMatrix2 :: HasCallStack => ATransport -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testBasicMatrix2 t runTest = do + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest + it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrevPQ agentCfgVPrevPQ 3 $ runTest + it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrevPQ agentCfg 3 $ runTest + it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrevPQ 3 $ runTest + testRatchetMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True - it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False - it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False - it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False - it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False - it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff False - it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff False - it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff False + it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True + it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False + it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 1 $ runTest PQSupportOff False + it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 1 $ runTest PQSupportOff False + it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 1 $ runTest PQSupportOff False testServerMatrix2 :: HasCallStack => ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -533,7 +519,7 @@ pqMatrix2_ pqInv t test = do it "pq-inv/dh handshake" $ runTest $ \a b -> test (a, IKUsePQ) (b, PQSupportOff) it "pq-inv/pq handshake" $ runTest $ \a b -> test (a, IKUsePQ) (b, PQSupportOn) where - runTest = withSmpServerProxy t . runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 + runTest = withSmpServerProxy t . runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 testPQMatrix3 :: HasCallStack => @@ -552,8 +538,8 @@ testPQMatrix3 t test = do where runTest test' = withSmpServerProxy t $ - runTestCfgServers2 agentProxyCfg agentProxyCfg servers 3 $ \a b baseMsgId -> - withAgent 3 agentProxyCfg servers testDB3 $ \c -> test' a b c baseMsgId + runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 servers 3 $ \a b baseMsgId -> + withAgent 3 agentProxyCfgV8 servers testDB3 $ \c -> test' a b c baseMsgId servers = initAgentServersProxy SPMAlways SPFProhibit runTestCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () @@ -637,48 +623,48 @@ testEnablePQEncryption = (aId, bId) <- makeConnection_ PQSupportOff ca cb let a = (ca, aId) b = (cb, bId) - (a, 4, "msg 1") \#>\ b - (b, 5, "msg 2") \#>\ a + (a, 2, "msg 1") \#>\ b + (b, 3, "msg 2") \#>\ a -- 45 bytes is used by agent message envelope inside double ratchet message envelope let largeMsg g' pqEnc = atomically $ C.randomBytes (e2eEncAgentMsgLength pqdrSMPAgentVersion pqEnc - 45) g' lrg <- largeMsg g PQSupportOff - (a, 6, lrg) \#>\ b - (b, 7, lrg) \#>\ a + (a, 4, lrg) \#>\ b + (b, 5, lrg) \#>\ a -- switched to smaller envelopes (before reporting PQ encryption enabled) sml <- largeMsg g PQSupportOn -- fail because of message size Left (A.CMD LARGE _) <- tryError $ A.sendMessage ca bId PQEncOn SMP.noMsgFlags lrg - (9, PQEncOff) <- A.sendMessage ca bId PQEncOn SMP.noMsgFlags sml - get ca =##> \case ("", connId, SENT 9) -> connId == bId; _ -> False - get cb =##> \case ("", connId, MsgErr' 8 MsgSkipped {} PQEncOff msg') -> connId == aId && msg' == sml; _ -> False - ackMessage cb aId 8 Nothing + (7, PQEncOff) <- A.sendMessage ca bId PQEncOn SMP.noMsgFlags sml + get ca =##> \case ("", connId, SENT 7) -> connId == bId; _ -> False + get cb =##> \case ("", connId, MsgErr' 6 MsgSkipped {} PQEncOff msg') -> connId == aId && msg' == sml; _ -> False + ackMessage cb aId 6 Nothing -- -- fail in reply to sync IDss Left (A.CMD LARGE _) <- tryError $ A.sendMessage cb aId PQEncOn SMP.noMsgFlags lrg - (10, PQEncOff) <- A.sendMessage cb aId PQEncOn SMP.noMsgFlags sml - get cb =##> \case ("", connId, SENT 10) -> connId == aId; _ -> False - get ca =##> \case ("", connId, MsgErr' 10 MsgSkipped {} PQEncOff msg') -> connId == bId && msg' == sml; _ -> False - ackMessage ca bId 10 Nothing - (a, 11, sml) \#>! b + (8, PQEncOff) <- A.sendMessage cb aId PQEncOn SMP.noMsgFlags sml + get cb =##> \case ("", connId, SENT 8) -> connId == aId; _ -> False + get ca =##> \case ("", connId, MsgErr' 8 MsgSkipped {} PQEncOff msg') -> connId == bId && msg' == sml; _ -> False + ackMessage ca bId 8 Nothing + (a, 9, sml) \#>! b -- PQ encryption now enabled + (b, 10, sml) !#>! a + (a, 11, sml) !#>! b (b, 12, sml) !#>! a - (a, 13, sml) !#>! b - (b, 14, sml) !#>! a -- disabling PQ encryption - (a, 15, sml) !#>\ b - (b, 16, sml) !#>\ a - (a, 17, sml) \#>\ b - (b, 18, sml) \#>\ a + (a, 13, sml) !#>\ b + (b, 14, sml) !#>\ a + (a, 15, sml) \#>\ b + (b, 16, sml) \#>\ a -- enabling PQ encryption again + (a, 17, sml) \#>! b + (b, 18, sml) \#>! a (a, 19, sml) \#>! b - (b, 20, sml) \#>! a - (a, 21, sml) \#>! b - (b, 22, sml) !#>! a - (a, 23, sml) !#>! b + (b, 20, sml) !#>! a + (a, 21, sml) !#>! b -- disabling PQ encryption again - (b, 24, sml) !#>\ a - (a, 25, sml) !#>\ b - (b, 26, sml) \#>\ a - (a, 27, sml) \#>\ b + (b, 22, sml) !#>\ a + (a, 23, sml) !#>\ b + (b, 24, sml) \#>\ a + (a, 25, sml) \#>\ b -- PQ encryption is now disabled, but support remained enabled, so we still cannot send larger messages Left (A.CMD LARGE _) <- tryError $ A.sendMessage ca bId PQEncOff SMP.noMsgFlags (sml <> "123456") Left (A.CMD LARGE _) <- tryError $ A.sendMessage cb aId PQEncOff SMP.noMsgFlags (sml <> "123456") @@ -703,22 +689,22 @@ testAgentClient3 = (aIdForB, bId) <- makeConnection a b (aIdForC, cId) <- makeConnection a c - 4 <- sendMessage a bId SMP.noMsgFlags "b4" - 4 <- sendMessage a cId SMP.noMsgFlags "c4" - 5 <- sendMessage a bId SMP.noMsgFlags "b5" - 5 <- sendMessage a cId SMP.noMsgFlags "c5" - get a =##> \case ("", connId, SENT 4) -> connId == bId || connId == cId; _ -> False - get a =##> \case ("", connId, SENT 4) -> connId == bId || connId == cId; _ -> False - get a =##> \case ("", connId, SENT 5) -> connId == bId || connId == cId; _ -> False - get a =##> \case ("", connId, SENT 5) -> connId == bId || connId == cId; _ -> False + 2 <- sendMessage a bId SMP.noMsgFlags "b4" + 2 <- sendMessage a cId SMP.noMsgFlags "c4" + 3 <- sendMessage a bId SMP.noMsgFlags "b5" + 3 <- sendMessage a cId SMP.noMsgFlags "c5" + get a =##> \case ("", connId, SENT 2) -> connId == bId || connId == cId; _ -> False + get a =##> \case ("", connId, SENT 2) -> connId == bId || connId == cId; _ -> False + get a =##> \case ("", connId, SENT 3) -> connId == bId || connId == cId; _ -> False + get a =##> \case ("", connId, SENT 3) -> connId == bId || connId == cId; _ -> False get b =##> \case ("", connId, Msg "b4") -> connId == aIdForB; _ -> False - ackMessage b aIdForB 4 Nothing + ackMessage b aIdForB 2 Nothing get b =##> \case ("", connId, Msg "b5") -> connId == aIdForB; _ -> False - ackMessage b aIdForB 5 Nothing + ackMessage b aIdForB 3 Nothing get c =##> \case ("", connId, Msg "c4") -> connId == aIdForC; _ -> False - ackMessage c aIdForC 4 Nothing + ackMessage c aIdForC 2 Nothing get c =##> \case ("", connId, Msg "c5") -> connId == aIdForC; _ -> False - ackMessage c aIdForC 5 Nothing + ackMessage c aIdForC 3 Nothing runAgentClientContactTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () runAgentClientContactTest pqSupport viaProxy alice bob baseId = @@ -920,7 +906,7 @@ testAllowConnectionClientRestart t = do runRight_ $ do allowConnectionAsync alice "1" bobId confId "alice's connInfo" - get alice =##> \case ("1", _, OK) -> True; _ -> False + get alice ##> ("1", bobId, OK) pure () threadDelay 100000 -- give time to enqueue confirmation (enqueueConfirmation) @@ -937,8 +923,7 @@ testAllowConnectionClientRestart t = do get alice2 ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) - - exchangeGreetingsMsgId 4 alice2 bobId bob aliceId + exchangeGreetings alice2 bobId bob aliceId disposeAgentClient alice2 disposeAgentClient bob @@ -949,7 +934,7 @@ testIncreaseConnAgentVersion t = do withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob - exchangeGreetingsMsgId_ PQEncOff 4 alice bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 2 alice bobId bob aliceId checkVersion alice bobId 2 checkVersion bob aliceId 2 pure (aliceId, bobId) @@ -961,7 +946,7 @@ testIncreaseConnAgentVersion t = do runRight_ $ do subscribeConnection alice2 bobId - exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 4 alice2 bobId bob aliceId checkVersion alice2 bobId 2 checkVersion bob aliceId 2 @@ -972,7 +957,7 @@ testIncreaseConnAgentVersion t = do runRight_ $ do subscribeConnection bob2 aliceId - exchangeGreetingsMsgId_ PQEncOff 8 alice2 bobId bob2 aliceId + exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob2 aliceId checkVersion alice2 bobId 3 checkVersion bob2 aliceId 3 @@ -983,7 +968,7 @@ testIncreaseConnAgentVersion t = do runRight_ $ do subscribeConnection alice3 bobId - exchangeGreetingsMsgId_ PQEncOff 10 alice3 bobId bob2 aliceId + exchangeGreetingsMsgId_ PQEncOff 8 alice3 bobId bob2 aliceId checkVersion alice3 bobId 3 checkVersion bob2 aliceId 3 @@ -992,7 +977,7 @@ testIncreaseConnAgentVersion t = do runRight_ $ do subscribeConnection bob3 aliceId - exchangeGreetingsMsgId_ PQEncOff 12 alice3 bobId bob3 aliceId + exchangeGreetingsMsgId_ PQEncOff 10 alice3 bobId bob3 aliceId checkVersion alice3 bobId 3 checkVersion bob3 aliceId 3 disposeAgentClient alice3 @@ -1010,7 +995,7 @@ testIncreaseConnAgentVersionMaxCompatible t = do withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob - exchangeGreetingsMsgId_ PQEncOff 4 alice bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 2 alice bobId bob aliceId checkVersion alice bobId 2 checkVersion bob aliceId 2 pure (aliceId, bobId) @@ -1025,7 +1010,7 @@ testIncreaseConnAgentVersionMaxCompatible t = do runRight_ $ do subscribeConnection alice2 bobId subscribeConnection bob2 aliceId - exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob2 aliceId + exchangeGreetingsMsgId_ PQEncOff 4 alice2 bobId bob2 aliceId checkVersion alice2 bobId 3 checkVersion bob2 aliceId 3 disposeAgentClient alice2 @@ -1038,7 +1023,7 @@ testIncreaseConnAgentVersionStartDifferentVersion t = do withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob - exchangeGreetingsMsgId_ PQEncOff 4 alice bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 2 alice bobId bob aliceId checkVersion alice bobId 2 checkVersion bob aliceId 2 pure (aliceId, bobId) @@ -1050,7 +1035,7 @@ testIncreaseConnAgentVersionStartDifferentVersion t = do runRight_ $ do subscribeConnection alice2 bobId - exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 4 alice2 bobId bob aliceId checkVersion alice2 bobId 3 checkVersion bob aliceId 3 disposeAgentClient alice2 @@ -1064,13 +1049,13 @@ testDeliverClientRestart t = do (aliceId, bobId) <- withSmpServerStoreMsgLogOn t testPort $ \_ -> do runRight $ do (aliceId, bobId) <- makeConnection alice bob - exchangeGreetingsMsgId 4 alice bobId bob aliceId + exchangeGreetings alice bobId bob aliceId pure (aliceId, bobId) ("", "", DOWN _ _) <- nGet alice ("", "", DOWN _ _) <- nGet bob - 6 <- runRight $ sendMessage bob aliceId SMP.noMsgFlags "hello" + 4 <- runRight $ sendMessage bob aliceId SMP.noMsgFlags "hello" disposeAgentClient bob @@ -1082,7 +1067,7 @@ testDeliverClientRestart t = do subscribeConnection bob2 aliceId - get bob2 ##> ("", aliceId, SENT 6) + get bob2 ##> ("", aliceId, SENT 4) get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False disposeAgentClient alice disposeAgentClient bob2 @@ -1094,8 +1079,8 @@ testDuplicateMessage t = do (aliceId, bobId, bob1) <- withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ makeConnection alice bob runRight_ $ do - 4 <- sendMessage alice bobId SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT 4) + 2 <- sendMessage alice bobId SMP.noMsgFlags "hello" + get alice ##> ("", bobId, SENT 2) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False disposeAgentClient bob @@ -1104,9 +1089,9 @@ testDuplicateMessage t = do runRight_ $ do subscribeConnection bob1 aliceId get bob1 =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False - ackMessage bob1 aliceId 4 Nothing - 5 <- sendMessage alice bobId SMP.noMsgFlags "hello 2" - get alice ##> ("", bobId, SENT 5) + ackMessage bob1 aliceId 2 Nothing + 3 <- sendMessage alice bobId SMP.noMsgFlags "hello 2" + get alice ##> ("", bobId, SENT 3) get bob1 =##> \case ("", c, Msg "hello 2") -> c == aliceId; _ -> False pure (aliceId, bobId, bob1) @@ -1116,7 +1101,7 @@ testDuplicateMessage t = do -- commenting two lines below and uncommenting further two lines would also runRight_, -- it is the scenario tested above, when the message was not acknowledged by the user threadDelay 200000 - Left (BROKER _ NETWORK) <- runExceptT $ ackMessage bob1 aliceId 5 Nothing + Left (BROKER _ NETWORK) <- runExceptT $ ackMessage bob1 aliceId 3 Nothing disposeAgentClient alice disposeAgentClient bob1 @@ -1131,8 +1116,8 @@ testDuplicateMessage t = do -- get bob2 =##> \case ("", c, Msg "hello 2") -> c == aliceId; _ -> False -- ackMessage bob2 aliceId 5 Nothing -- message 2 is not delivered again, even though it was delivered to the agent - 6 <- sendMessage alice2 bobId SMP.noMsgFlags "hello 3" - get alice2 ##> ("", bobId, SENT 6) + 4 <- sendMessage alice2 bobId SMP.noMsgFlags "hello 3" + get alice2 ##> ("", bobId, SENT 4) get bob2 =##> \case ("", c, Msg "hello 3") -> c == aliceId; _ -> False disposeAgentClient alice2 disposeAgentClient bob2 @@ -1144,20 +1129,20 @@ testSkippedMessages t = do (aliceId, bobId) <- withSmpServerStoreLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ makeConnection alice bob runRight_ $ do - 4 <- sendMessage alice bobId SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT 4) + 2 <- sendMessage alice bobId SMP.noMsgFlags "hello" + get alice ##> ("", bobId, SENT 2) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False - ackMessage bob aliceId 4 Nothing + ackMessage bob aliceId 2 Nothing disposeAgentClient bob runRight_ $ do - 5 <- sendMessage alice bobId SMP.noMsgFlags "hello 2" + 3 <- sendMessage alice bobId SMP.noMsgFlags "hello 2" + get alice ##> ("", bobId, SENT 3) + 4 <- sendMessage alice bobId SMP.noMsgFlags "hello 3" + get alice ##> ("", bobId, SENT 4) + 5 <- sendMessage alice bobId SMP.noMsgFlags "hello 4" get alice ##> ("", bobId, SENT 5) - 6 <- sendMessage alice bobId SMP.noMsgFlags "hello 3" - get alice ##> ("", bobId, SENT 6) - 7 <- sendMessage alice bobId SMP.noMsgFlags "hello 4" - get alice ##> ("", bobId, SENT 7) pure (aliceId, bobId) @@ -1174,15 +1159,15 @@ testSkippedMessages t = do subscribeConnection bob2 aliceId subscribeConnection alice2 bobId - 8 <- sendMessage alice2 bobId SMP.noMsgFlags "hello 5" - get alice2 ##> ("", bobId, SENT 8) - get bob2 =##> \case ("", c, MSG MsgMeta {integrity = MsgError {errorInfo = MsgSkipped {fromMsgId = 4, toMsgId = 6}}} _ "hello 5") -> c == aliceId; _ -> False - ackMessage bob2 aliceId 5 Nothing + 6 <- sendMessage alice2 bobId SMP.noMsgFlags "hello 5" + get alice2 ##> ("", bobId, SENT 6) + get bob2 =##> \case ("", c, MSG MsgMeta {integrity = MsgError {errorInfo = MsgSkipped {fromMsgId = 3, toMsgId = 5}}} _ "hello 5") -> c == aliceId; _ -> False + ackMessage bob2 aliceId 3 Nothing - 9 <- sendMessage alice2 bobId SMP.noMsgFlags "hello 6" - get alice2 ##> ("", bobId, SENT 9) + 7 <- sendMessage alice2 bobId SMP.noMsgFlags "hello 6" + get alice2 ##> ("", bobId, SENT 7) get bob2 =##> \case ("", c, Msg "hello 6") -> c == aliceId; _ -> False - ackMessage bob2 aliceId 6 Nothing + ackMessage bob2 aliceId 4 Nothing disposeAgentClient alice2 disposeAgentClient bob2 @@ -1192,7 +1177,7 @@ testDeliveryAfterSubscriptionError t = do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ makeConnection a b nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False - 4 <- runRight $ sendMessage a bId SMP.noMsgFlags "hello" + 2 <- runRight $ sendMessage a bId SMP.noMsgFlags "hello" liftIO $ noMessages b "not delivered" pure (aId, bId) @@ -1201,9 +1186,9 @@ testDeliveryAfterSubscriptionError t = do Left (BROKER _ NETWORK) <- runExceptT $ subscribeConnection b aId pure () withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do - withUP a bId $ \case ("", c, SENT 4) -> c == bId; _ -> False + withUP a bId $ \case ("", c, SENT 2) -> c == bId; _ -> False withUP b aId $ \case ("", c, Msg "hello") -> c == aId; _ -> False - ackMessage b aId 4 Nothing + ackMessage b aId 2 Nothing testMsgDeliveryQuotaExceeded :: HasCallStack => ATransport -> IO () testMsgDeliveryQuotaExceeded t = @@ -1213,24 +1198,24 @@ testMsgDeliveryQuotaExceeded t = forM_ ([1 .. 4] :: [Int]) $ \i -> do mId <- sendMessage a bId SMP.noMsgFlags $ "message " <> bshow i get a =##> \case ("", c, SENT mId') -> bId == c && mId == mId'; _ -> False - 8 <- sendMessage a bId SMP.noMsgFlags "over quota" - pGet' a False =##> \case ("", c, AEvt _ (MWARN 8 (SMP _ QUOTA))) -> bId == c; _ -> False - 4 <- sendMessage a bId' SMP.noMsgFlags "hello" - get a =##> \case ("", c, SENT 4) -> bId' == c; _ -> False + 6 <- sendMessage a bId SMP.noMsgFlags "over quota" + pGet' a False =##> \case ("", c, AEvt _ (MWARN 6 (SMP _ QUOTA))) -> bId == c; _ -> False + 2 <- sendMessage a bId' SMP.noMsgFlags "hello" + get a =##> \case ("", c, SENT 2) -> bId' == c; _ -> False get b =##> \case ("", c, Msg "message 1") -> aId == c; _ -> False get b =##> \case ("", c, Msg "hello") -> aId' == c; _ -> False - ackMessage b aId' 4 Nothing - ackMessage b aId 4 Nothing + ackMessage b aId' 2 Nothing + ackMessage b aId 2 Nothing get b =##> \case ("", c, Msg "message 2") -> aId == c; _ -> False - ackMessage b aId 5 Nothing + ackMessage b aId 3 Nothing get b =##> \case ("", c, Msg "message 3") -> aId == c; _ -> False - ackMessage b aId 6 Nothing + ackMessage b aId 4 Nothing get b =##> \case ("", c, Msg "message 4") -> aId == c; _ -> False - ackMessage b aId 7 Nothing + ackMessage b aId 5 Nothing get a =##> \case ("", c, QCONT) -> bId == c; _ -> False get b =##> \case ("", c, Msg "over quota") -> aId == c; _ -> False - ackMessage b aId 9 Nothing -- msg 8 was QCONT - get a =##> \case ("", c, SENT 8) -> bId == c; _ -> False + ackMessage b aId 7 Nothing -- msg 8 was QCONT + get a =##> \case ("", c, SENT 6) -> bId == c; _ -> False liftIO $ concurrently_ (noMessages a "no more events") (noMessages b "no more events") testExpireMessage :: HasCallStack => ATransport -> IO () @@ -1240,14 +1225,14 @@ testExpireMessage t = (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ makeConnection a b nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False - 4 <- runRight $ sendMessage a bId SMP.noMsgFlags "1" + 2 <- runRight $ sendMessage a bId SMP.noMsgFlags "1" threadDelay 1000000 - 5 <- runRight $ sendMessage a bId SMP.noMsgFlags "2" -- this won't expire - get a =##> \case ("", c, MERR 4 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False + 3 <- runRight $ sendMessage a bId SMP.noMsgFlags "2" -- this won't expire + get a =##> \case ("", c, MERR 2 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do - withUP a bId $ \case ("", _, SENT 5) -> True; _ -> False - withUP b aId $ \case ("", _, MsgErr 4 (MsgSkipped 3 3) "2") -> True; _ -> False - ackMessage b aId 4 Nothing + withUP a bId $ \case ("", _, SENT 3) -> True; _ -> False + withUP b aId $ \case ("", _, MsgErr 2 (MsgSkipped 2 2) "2") -> True; _ -> False + ackMessage b aId 2 Nothing testExpireManyMessages :: HasCallStack => ATransport -> IO () testExpireManyMessages t = @@ -1257,28 +1242,28 @@ testExpireManyMessages t = runRight_ $ do nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False - 4 <- sendMessage a bId SMP.noMsgFlags "1" - 5 <- sendMessage a bId SMP.noMsgFlags "2" - 6 <- sendMessage a bId SMP.noMsgFlags "3" + 2 <- sendMessage a bId SMP.noMsgFlags "1" + 3 <- sendMessage a bId SMP.noMsgFlags "2" + 4 <- sendMessage a bId SMP.noMsgFlags "3" liftIO $ threadDelay 1000000 - 7 <- sendMessage a bId SMP.noMsgFlags "4" -- this won't expire - get a =##> \case ("", c, MERR 4 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False + 5 <- sendMessage a bId SMP.noMsgFlags "4" -- this won't expire + get a =##> \case ("", c, MERR 2 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False -- get a =##> \case ("", c, MERRS [5, 6] (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False let expected c e = bId == c && (e == TIMEOUT || e == NETWORK) get a >>= \case - ("", c, MERR 5 (BROKER _ e)) -> do + ("", c, MERR 3 (BROKER _ e)) -> do liftIO $ expected c e `shouldBe` True - get a =##> \case ("", c', MERR 6 (BROKER _ e')) -> expected c' e'; ("", c', MERRS [6] (BROKER _ e')) -> expected c' e'; _ -> False - ("", c, MERRS [5] (BROKER _ e)) -> do + get a =##> \case ("", c', MERR 4 (BROKER _ e')) -> expected c' e'; ("", c', MERRS [6] (BROKER _ e')) -> expected c' e'; _ -> False + ("", c, MERRS [3] (BROKER _ e)) -> do liftIO $ expected c e `shouldBe` True - get a =##> \case ("", c', MERR 6 (BROKER _ e')) -> expected c' e'; _ -> False - ("", c, MERRS [5, 6] (BROKER _ e)) -> + get a =##> \case ("", c', MERR 4 (BROKER _ e')) -> expected c' e'; _ -> False + ("", c, MERRS [3, 4] (BROKER _ e)) -> liftIO $ expected c e `shouldBe` True r -> error $ show r withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do - withUP a bId $ \case ("", _, SENT 7) -> True; _ -> False - withUP b aId $ \case ("", _, MsgErr 4 (MsgSkipped 3 5) "4") -> True; _ -> False - ackMessage b aId 4 Nothing + withUP a bId $ \case ("", _, SENT 5) -> True; _ -> False + withUP b aId $ \case ("", _, MsgErr 2 (MsgSkipped 2 4) "4") -> True; _ -> False + ackMessage b aId 2 Nothing withUP :: AgentClient -> ConnId -> (AEntityTransmission 'AEConn -> Bool) -> ExceptT AgentErrorType IO () withUP a bId p = @@ -1296,23 +1281,23 @@ testExpireMessageQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testP (aId, bId) <- runRight $ do (aId, bId) <- makeConnection a b liftIO $ threadDelay 500000 >> disposeAgentClient b - 4 <- sendMessage a bId SMP.noMsgFlags "1" - get a ##> ("", bId, SENT 4) - 5 <- sendMessage a bId SMP.noMsgFlags "2" + 2 <- sendMessage a bId SMP.noMsgFlags "1" + get a ##> ("", bId, SENT 2) + 3 <- sendMessage a bId SMP.noMsgFlags "2" liftIO $ threadDelay 1000000 - 6 <- sendMessage a bId SMP.noMsgFlags "3" -- this won't expire - get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False + 4 <- sendMessage a bId SMP.noMsgFlags "3" -- this won't expire + get a =##> \case ("", c, MERR 3 (SMP _ QUOTA)) -> bId == c; _ -> False pure (aId, bId) withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do subscribeConnection b' aId get b' =##> \case ("", c, Msg "1") -> c == aId; _ -> False - ackMessage b' aId 4 Nothing + ackMessage b' aId 2 Nothing liftIO . getInAnyOrder a $ - [ \case ("", c, AEvt SAEConn (SENT 6)) -> c == bId; _ -> False, + [ \case ("", c, AEvt SAEConn (SENT 4)) -> c == bId; _ -> False, \case ("", c, AEvt SAEConn QCONT) -> c == bId; _ -> False ] - get b' =##> \case ("", c, MsgErr 6 (MsgSkipped 4 4) "3") -> c == aId; _ -> False - ackMessage b' aId 6 Nothing + get b' =##> \case ("", c, MsgErr 4 (MsgSkipped 3 3) "3") -> c == aId; _ -> False + ackMessage b' aId 4 Nothing disposeAgentClient a testExpireManyMessagesQuota :: ATransport -> IO () @@ -1322,34 +1307,34 @@ testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} (aId, bId) <- runRight $ do (aId, bId) <- makeConnection a b liftIO $ threadDelay 500000 >> disposeAgentClient b - 4 <- sendMessage a bId SMP.noMsgFlags "1" - get a ##> ("", bId, SENT 4) - 5 <- sendMessage a bId SMP.noMsgFlags "2" - 6 <- sendMessage a bId SMP.noMsgFlags "3" - 7 <- sendMessage a bId SMP.noMsgFlags "4" + 2 <- sendMessage a bId SMP.noMsgFlags "1" + get a ##> ("", bId, SENT 2) + 3 <- sendMessage a bId SMP.noMsgFlags "2" + 4 <- sendMessage a bId SMP.noMsgFlags "3" + 5 <- sendMessage a bId SMP.noMsgFlags "4" liftIO $ threadDelay 1000000 - 8 <- sendMessage a bId SMP.noMsgFlags "5" -- this won't expire - get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False + 6 <- sendMessage a bId SMP.noMsgFlags "5" -- this won't expire + get a =##> \case ("", c, MERR 3 (SMP _ QUOTA)) -> bId == c; _ -> False get a >>= \case - ("", c, MERR 6 (SMP _ QUOTA)) -> do + ("", c, MERR 4 (SMP _ QUOTA)) -> do liftIO $ bId `shouldBe` c - get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP _ QUOTA)) -> bId == c'; _ -> False - ("", c, MERRS [6] (SMP _ QUOTA)) -> do + get a =##> \case ("", c', MERR 5 (SMP _ QUOTA)) -> bId == c'; ("", c', MERRS [5] (SMP _ QUOTA)) -> bId == c'; _ -> False + ("", c, MERRS [4] (SMP _ QUOTA)) -> do liftIO $ bId `shouldBe` c - get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; _ -> False - ("", c, MERRS [6, 7] (SMP _ QUOTA)) -> liftIO $ bId `shouldBe` c + get a =##> \case ("", c', MERR 5 (SMP _ QUOTA)) -> bId == c'; _ -> False + ("", c, MERRS [4, 5] (SMP _ QUOTA)) -> liftIO $ bId `shouldBe` c r -> error $ show r pure (aId, bId) withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do subscribeConnection b' aId get b' =##> \case ("", c, Msg "1") -> c == aId; _ -> False - ackMessage b' aId 4 Nothing + ackMessage b' aId 2 Nothing liftIO . getInAnyOrder a $ - [ \case ("", c, AEvt SAEConn (SENT 8)) -> c == bId; _ -> False, + [ \case ("", c, AEvt SAEConn (SENT 6)) -> c == bId; _ -> False, \case ("", c, AEvt SAEConn QCONT) -> c == bId; _ -> False ] - get b' =##> \case ("", c, MsgErr 6 (MsgSkipped 4 6) "5") -> c == aId; _ -> False - ackMessage b' aId 6 Nothing + get b' =##> \case ("", c, MsgErr 4 (MsgSkipped 3 5) "5") -> c == aId; _ -> False + ackMessage b' aId 4 Nothing disposeAgentClient a testRatchetSync :: HasCallStack => ATransport -> IO () @@ -1363,34 +1348,34 @@ testRatchetSync t = withAgentClients2 $ \alice bob -> get bob2 =##> ratchetSyncP aliceId RSAgreed get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk - exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 + exchangeGreetingsMsgIds alice bobId 10 bob2 aliceId 7 disposeAgentClient bob2 setupDesynchronizedRatchet :: HasCallStack => AgentClient -> AgentClient -> IO (ConnId, ConnId, AgentClient) setupDesynchronizedRatchet alice bob = do (aliceId, bobId) <- runRight $ makeConnection alice bob runRight_ $ do - 4 <- sendMessage alice bobId SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT 4) + 2 <- sendMessage alice bobId SMP.noMsgFlags "hello" + get alice ##> ("", bobId, SENT 2) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False - ackMessage bob aliceId 4 Nothing + ackMessage bob aliceId 2 Nothing - 5 <- sendMessage bob aliceId SMP.noMsgFlags "hello 2" - get bob ##> ("", aliceId, SENT 5) + 3 <- sendMessage bob aliceId SMP.noMsgFlags "hello 2" + get bob ##> ("", aliceId, SENT 3) get alice =##> \case ("", c, Msg "hello 2") -> c == bobId; _ -> False - ackMessage alice bobId 5 Nothing + ackMessage alice bobId 3 Nothing liftIO $ copyFile testDB2 (testDB2 <> ".bak") - 6 <- sendMessage alice bobId SMP.noMsgFlags "hello 3" - get alice ##> ("", bobId, SENT 6) + 4 <- sendMessage alice bobId SMP.noMsgFlags "hello 3" + get alice ##> ("", bobId, SENT 4) get bob =##> \case ("", c, Msg "hello 3") -> c == aliceId; _ -> False - ackMessage bob aliceId 6 Nothing + ackMessage bob aliceId 4 Nothing - 7 <- sendMessage bob aliceId SMP.noMsgFlags "hello 4" - get bob ##> ("", aliceId, SENT 7) + 5 <- sendMessage bob aliceId SMP.noMsgFlags "hello 4" + get bob ##> ("", aliceId, SENT 5) get alice =##> \case ("", c, Msg "hello 4") -> c == bobId; _ -> False - ackMessage alice bobId 7 Nothing + ackMessage alice bobId 5 Nothing disposeAgentClient bob @@ -1404,8 +1389,8 @@ setupDesynchronizedRatchet alice bob = do Left A.CMD {cmdErr = PROHIBITED} <- liftIO . runExceptT $ synchronizeRatchet bob2 aliceId PQSupportOn False - 8 <- sendMessage alice bobId SMP.noMsgFlags "hello 5" - get alice ##> ("", bobId, SENT 8) + 6 <- sendMessage alice bobId SMP.noMsgFlags "hello 5" + get alice ##> ("", bobId, SENT 6) get bob2 =##> ratchetSyncP aliceId RSRequired Left A.CMD {cmdErr = PROHIBITED} <- liftIO . runExceptT $ sendMessage bob2 aliceId SMP.noMsgFlags "hello 6" @@ -1443,7 +1428,7 @@ testRatchetSyncServerOffline t = withAgentClients2 $ \alice bob -> do runRight_ $ do get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk - exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 + exchangeGreetingsMsgIds alice bobId 10 bob2 aliceId 7 disposeAgentClient bob2 serverUpP :: ATransmission -> Bool @@ -1471,7 +1456,7 @@ testRatchetSyncClientRestart t = do get bob3 =##> ratchetSyncP aliceId RSAgreed get alice =##> ratchetSyncP bobId RSOk get bob3 =##> ratchetSyncP aliceId RSOk - exchangeGreetingsMsgIds alice bobId 12 bob3 aliceId 9 + exchangeGreetingsMsgIds alice bobId 10 bob3 aliceId 7 disposeAgentClient alice disposeAgentClient bob disposeAgentClient bob3 @@ -1500,7 +1485,7 @@ testRatchetSyncSuspendForeground t = do runRight_ $ do get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk - exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 + exchangeGreetingsMsgIds alice bobId 10 bob2 aliceId 7 disposeAgentClient alice disposeAgentClient bob disposeAgentClient bob2 @@ -1528,13 +1513,13 @@ testRatchetSyncSimultaneous t = do runRight_ $ do get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk - exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 + exchangeGreetingsMsgIds alice bobId 10 bob2 aliceId 7 disposeAgentClient alice disposeAgentClient bob disposeAgentClient bob2 -testOnlyCreatePull :: IO () -testOnlyCreatePull = withAgentClients2 $ \alice bob -> runRight_ $ do +testOnlyCreatePullSlowHandshake :: IO () +testOnlyCreatePullSlowHandshake = withAgentClientsCfg2 agentProxyCfgV8 agentProxyCfgV8 $ \alice bob -> runRight_ $ do (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMOnlyCreate aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMOnlyCreate Just ("", _, CONF confId _ "bob's connInfo") <- getMsg alice bobId $ timeout 5_000000 $ get alice @@ -1558,14 +1543,38 @@ testOnlyCreatePull = withAgentClients2 $ \alice bob -> runRight_ $ do getMsg alice bobId $ get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False ackMessage alice bobId 5 Nothing - where - getMsg :: AgentClient -> ConnId -> ExceptT AgentErrorType IO a -> ExceptT AgentErrorType IO a - getMsg c cId action = do - liftIO $ noMessages c "nothing should be delivered before GET" - Just _ <- getConnectionMessage c cId - r <- action - get c =##> \case ("", cId', MSGNTF _) -> cId == cId'; _ -> False - pure r + +getMsg :: AgentClient -> ConnId -> ExceptT AgentErrorType IO a -> ExceptT AgentErrorType IO a +getMsg c cId action = do + liftIO $ noMessages c "nothing should be delivered before GET" + Just _ <- getConnectionMessage c cId + r <- action + get c =##> \case ("", cId', MSGNTF _) -> cId == cId'; _ -> False + pure r + +testOnlyCreatePull :: IO () +testOnlyCreatePull = withAgentClients2 $ \alice bob -> runRight_ $ do + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMOnlyCreate + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMOnlyCreate + Just ("", _, CONF confId _ "bob's connInfo") <- getMsg alice bobId $ timeout 5_000000 $ get alice + allowConnection alice bobId confId "alice's connInfo" + liftIO $ threadDelay 1_000000 + getMsg bob aliceId $ do + get bob ##> ("", aliceId, INFO "alice's connInfo") + get bob ##> ("", aliceId, CON) + liftIO $ threadDelay 1_000000 + get alice ##> ("", bobId, CON) -- sent to initiating party after sending confirmation + -- exchange messages + 2 <- sendMessage alice bobId SMP.noMsgFlags "hello" + get alice ##> ("", bobId, SENT 2) + getMsg bob aliceId $ + get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + ackMessage bob aliceId 2 Nothing + 3 <- sendMessage bob aliceId SMP.noMsgFlags "hello too" + get bob ##> ("", aliceId, SENT 3) + getMsg alice bobId $ + get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + ackMessage alice bobId 3 Nothing makeConnection :: AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnection = makeConnection_ PQSupportOn @@ -1643,14 +1652,14 @@ testSuspendingAgent :: IO () testSuspendingAgent = withAgentClients2 $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b - 4 <- sendMessage a bId SMP.noMsgFlags "hello" - get a ##> ("", bId, SENT 4) + 2 <- sendMessage a bId SMP.noMsgFlags "hello" + get a ##> ("", bId, SENT 2) get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False - ackMessage b aId 4 Nothing + ackMessage b aId 2 Nothing liftIO $ suspendAgent b 1000000 get' b ##> ("", "", SUSPENDED) - 5 <- sendMessage a bId SMP.noMsgFlags "hello 2" - get a ##> ("", bId, SENT 5) + 3 <- sendMessage a bId SMP.noMsgFlags "hello 2" + get a ##> ("", bId, SENT 3) Nothing <- 100000 `timeout` get b liftIO $ foregroundAgent b get b =##> \case ("", c, Msg "hello 2") -> c == aId; _ -> False @@ -1659,47 +1668,47 @@ testSuspendingAgentCompleteSending :: ATransport -> IO () testSuspendingAgentCompleteSending t = withAgentClients2 $ \a b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do (aId, bId) <- makeConnection a b - 4 <- sendMessage a bId SMP.noMsgFlags "hello" - get a ##> ("", bId, SENT 4) + 2 <- sendMessage a bId SMP.noMsgFlags "hello" + get a ##> ("", bId, SENT 2) get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False - ackMessage b aId 4 Nothing + ackMessage b aId 2 Nothing pure (aId, bId) runRight_ $ do ("", "", DOWN {}) <- nGet a ("", "", DOWN {}) <- nGet b - 5 <- sendMessage b aId SMP.noMsgFlags "hello too" - 6 <- sendMessage b aId SMP.noMsgFlags "how are you?" + 3 <- sendMessage b aId SMP.noMsgFlags "hello too" + 4 <- sendMessage b aId SMP.noMsgFlags "how are you?" liftIO $ threadDelay 100000 liftIO $ suspendAgent b 5000000 withSmpServerStoreLogOn t testPort $ \_ -> runRight_ @AgentErrorType $ do - pGet b =##> \case ("", c, AEvt SAEConn (SENT 5)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, AEvt SAEConn (SENT 5)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, AEvt SAEConn (SENT 6)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, AEvt SAEConn (SENT 3)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, AEvt SAEConn (SENT 3)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, AEvt SAEConn (SENT 4)) -> c == aId; ("", "", AEvt _ UP {}) -> True; _ -> False ("", "", SUSPENDED) <- nGet b pGet a =##> \case ("", c, AEvt _ (Msg "hello too")) -> c == bId; ("", "", AEvt _ UP {}) -> True; _ -> False pGet a =##> \case ("", c, AEvt _ (Msg "hello too")) -> c == bId; ("", "", AEvt _ UP {}) -> True; _ -> False - ackMessage a bId 5 Nothing + ackMessage a bId 3 Nothing get a =##> \case ("", c, Msg "how are you?") -> c == bId; _ -> False - ackMessage a bId 6 Nothing + ackMessage a bId 4 Nothing testSuspendingAgentTimeout :: ATransport -> IO () testSuspendingAgentTimeout t = withAgentClients2 $ \a b -> do (aId, _) <- withSmpServer t . runRight $ do (aId, bId) <- makeConnection a b - 4 <- sendMessage a bId SMP.noMsgFlags "hello" - get a ##> ("", bId, SENT 4) + 2 <- sendMessage a bId SMP.noMsgFlags "hello" + get a ##> ("", bId, SENT 2) get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False - ackMessage b aId 4 Nothing + ackMessage b aId 2 Nothing pure (aId, bId) runRight_ $ do ("", "", DOWN {}) <- nGet a ("", "", DOWN {}) <- nGet b - 5 <- sendMessage b aId SMP.noMsgFlags "hello too" - 6 <- sendMessage b aId SMP.noMsgFlags "how are you?" + 3 <- sendMessage b aId SMP.noMsgFlags "hello too" + 4 <- sendMessage b aId SMP.noMsgFlags "how are you?" liftIO $ suspendAgent b 100000 ("", "", SUSPENDED) <- nGet b pure () @@ -1730,10 +1739,10 @@ testBatchedSubscriptions nCreate nDel t = (aIds', bIds') = unzip conns' subscribe a bIds subscribe b aIds - forM_ conns' $ \(aId, bId) -> exchangeGreetingsMsgId_ PQEncOff 6 a bId b aId + forM_ conns' $ \(aId, bId) -> exchangeGreetingsMsgId_ PQEncOff 4 a bId b aId void $ resubscribeConnections a bIds void $ resubscribeConnections b aIds - forM_ conns' $ \(aId, bId) -> exchangeGreetingsMsgId_ PQEncOff 8 a bId b aId + forM_ conns' $ \(aId, bId) -> exchangeGreetingsMsgId_ PQEncOff 6 a bId b aId delete a bIds' delete b aIds' deleteFail a bIds' @@ -1786,9 +1795,9 @@ testBatchedPendingMessages nCreate nMsgs = withA = withAgent 1 agentCfg initAgentServers testDB withB = withAgent 2 agentCfg initAgentServers testDB2 -testAsyncCommands :: IO () -testAsyncCommands = - withAgentClients2 $ \alice bob -> runRight_ $ do +testAsyncCommands :: AgentClient -> AgentClient -> AgentMsgId -> IO () +testAsyncCommands alice bob baseId = + runRight_ $ do bobId <- createConnectionAsync alice 1 "1" True SCMInvitation (IKNoPQ PQSupportOn) SMSubscribe ("1", bobId', INV (ACR _ qInfo)) <- get alice liftIO $ bobId' `shouldBe` bobId @@ -1833,7 +1842,6 @@ testAsyncCommands = get alice =##> \case ("", c, DEL_CONN) -> c == bobId; _ -> False liftIO $ noMessages alice "nothing else should be delivered to alice" where - baseId = 3 msgId = subtract baseId testAsyncCommandsRestore :: ATransport -> IO () @@ -1848,9 +1856,9 @@ testAsyncCommandsRestore t = do get alice' =##> \case ("1", _, INV _) -> True; _ -> False pure () -testAcceptContactAsync :: IO () -testAcceptContactAsync = - withAgentClients2 $ \alice bob -> runRight_ $ do +testAcceptContactAsync :: AgentClient -> AgentClient -> AgentMsgId -> IO () +testAcceptContactAsync alice bob baseId = + runRight_ $ do (_, qInfo) <- createConnection alice 1 True SCMContact Nothing SMSubscribe aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, REQ invId _ "bob's connInfo") <- get alice @@ -1884,7 +1892,6 @@ testAcceptContactAsync = deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where - baseId = 3 msgId = subtract baseId testDeleteConnectionAsync :: ATransport -> IO () @@ -1931,7 +1938,7 @@ testWaitDeliveryNoPending t = withAgentClients2 $ \alice bob -> liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDelivery :: ATransport -> IO () @@ -1985,7 +1992,7 @@ testWaitDelivery t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDeliveryAUTHErr :: ATransport -> IO () @@ -2028,7 +2035,7 @@ testWaitDeliveryAUTHErr t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDeliveryTimeout :: ATransport -> IO () @@ -2068,7 +2075,7 @@ testWaitDeliveryTimeout t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDeliveryTimeout2 :: ATransport -> IO () @@ -2114,14 +2121,14 @@ testWaitDeliveryTimeout2 t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId -testJoinConnectionAsyncReplyError :: HasCallStack => ATransport -> IO () -testJoinConnectionAsyncReplyError t = do +testJoinConnectionAsyncReplyErrorV8 :: HasCallStack => ATransport -> IO () +testJoinConnectionAsyncReplyErrorV8 t = do let initAgentServersSrv2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer2]} - withAgent 1 agentCfg initAgentServers testDB $ \a -> - withAgent 2 agentCfg initAgentServersSrv2 testDB2 $ \b -> do + withAgent 1 agentCfgVPrevPQ initAgentServers testDB $ \a -> + withAgent 2 agentCfgVPrevPQ initAgentServersSrv2 testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do bId <- createConnectionAsync a 1 "1" True SCMInvitation (IKNoPQ PQSupportOn) SMSubscribe ("1", bId', INV (ACR _ qInfo)) <- get a @@ -2145,52 +2152,92 @@ testJoinConnectionAsyncReplyError t = do nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False runRight_ $ do allowConnectionAsync a "3" bId confId "alice's connInfo" + get a ##> ("3", bId, OK) liftIO $ threadDelay 500000 ConnectionStats {rcvQueuesInfo = [RcvQueueInfo {}], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId pure () withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do - pGet a =##> \case ("3", c, AEvt _ OK) -> c == bId; ("", "", AEvt _ (UP _ [c])) -> c == bId; _ -> False - pGet a =##> \case ("3", c, AEvt _ OK) -> c == bId; ("", "", AEvt _ (UP _ [c])) -> c == bId; _ -> False + nGet a =##> \case ("", "", UP _ [c]) -> c == bId; _ -> False get a ##> ("", bId, CON) get b ##> ("", aId, INFO "alice's connInfo") get b ##> ("", aId, CON) + exchangeGreetingsMsgId 4 a bId b aId + +testJoinConnectionAsyncReplyError :: HasCallStack => ATransport -> IO () +testJoinConnectionAsyncReplyError t = do + let initAgentServersSrv2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer2]} + withAgent 1 agentCfg initAgentServers testDB $ \a -> + withAgent 2 agentCfg initAgentServersSrv2 testDB2 $ \b -> do + (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do + bId <- createConnectionAsync a 1 "1" True SCMInvitation (IKNoPQ PQSupportOn) SMSubscribe + ("1", bId', INV (ACR _ qInfo)) <- get a + liftIO $ bId' `shouldBe` bId + aId <- joinConnectionAsync b 1 "2" True qInfo "bob's connInfo" PQSupportOn SMSubscribe + liftIO $ threadDelay 500000 + ConnectionStats {rcvQueuesInfo = [], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId + pure (aId, bId) + nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False + withSmpServerOn t testPort2 $ do + confId <- withSmpServerStoreLogOn t testPort $ \_ -> do + -- both servers need to be online for connection to progress because of SKEY + get b =##> \case ("2", c, OK) -> c == aId; _ -> False + pGet a >>= \case + ("", "", AEvt _ (UP _ [_])) -> do + ("", _, CONF confId _ "bob's connInfo") <- get a + pure confId + ("", _, AEvt _ (CONF confId _ "bob's connInfo")) -> do + ("", "", UP _ [_]) <- nGet a + pure confId + r -> error $ "unexpected response " <> show r + nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False + runRight_ $ do + allowConnectionAsync a "3" bId confId "alice's connInfo" + get a ##> ("3", bId, OK) + get a ##> ("", bId, CON) + liftIO $ threadDelay 500000 + ConnectionStats {rcvQueuesInfo = [RcvQueueInfo {}], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId + pure () + withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do + nGet a =##> \case ("", "", UP _ [c]) -> c == bId; _ -> False + get b ##> ("", aId, INFO "alice's connInfo") + get b ##> ("", aId, CON) exchangeGreetings a bId b aId testUsers :: IO () testUsers = withAgentClients2 $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId auId <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] (aId', bId') <- makeConnectionForUsers a auId b 1 - exchangeGreetingsMsgId 4 a bId' b aId' + exchangeGreetings a bId' b aId' deleteUser a auId True get a =##> \case ("", c, DEL_RCVQ _ _ Nothing) -> c == bId'; _ -> False get a =##> \case ("", c, DEL_CONN) -> c == bId'; _ -> False nGet a =##> \case ("", "", DEL_USER u) -> u == auId; _ -> False - exchangeGreetingsMsgId 6 a bId b aId + exchangeGreetingsMsgId 4 a bId b aId liftIO $ noMessages a "nothing else should be delivered to alice" testDeleteUserQuietly :: IO () testDeleteUserQuietly = withAgentClients2 $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId auId <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] (aId', bId') <- makeConnectionForUsers a auId b 1 - exchangeGreetingsMsgId 4 a bId' b aId' + exchangeGreetings a bId' b aId' deleteUser a auId False - exchangeGreetingsMsgId 6 a bId b aId + exchangeGreetingsMsgId 4 a bId b aId liftIO $ noMessages a "nothing else should be delivered to alice" testUsersNoServer :: HasCallStack => ATransport -> IO () testUsersNoServer t = withAgentClientsCfg2 aCfg agentCfg $ \a b -> do (aId, bId, auId, _aId', bId') <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId auId <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] (aId', bId') <- makeConnectionForUsers a auId b 1 - exchangeGreetingsMsgId 4 a bId' b aId' + exchangeGreetings a bId' b aId' pure (aId, bId, auId, aId', bId') nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId || c == bId'; _ -> False nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId || c == bId'; _ -> False @@ -2204,7 +2251,7 @@ testUsersNoServer t = withAgentClientsCfg2 aCfg agentCfg $ \a b -> do withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do nGet a =##> \case ("", "", UP _ [c]) -> c == bId; _ -> False nGet b =##> \case ("", "", UP _ cs) -> length cs == 2; _ -> False - exchangeGreetingsMsgId 6 a bId b aId + exchangeGreetingsMsgId 4 a bId b aId where aCfg = agentCfg {initialCleanupDelay = 10000, cleanupInterval = 10000, deleteErrorCount = 3} @@ -2212,9 +2259,9 @@ testSwitchConnection :: InitialAgentServers -> IO () testSwitchConnection servers = withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId - testFullSwitch a bId b aId 10 - testFullSwitch a bId b aId 16 + exchangeGreetings a bId b aId + testFullSwitch a bId b aId 8 + testFullSwitch a bId b aId 14 testFullSwitch :: AgentClient -> ByteString -> AgentClient -> ByteString -> Int64 -> ExceptT AgentErrorType IO () testFullSwitch a bId b aId msgId = do @@ -2265,7 +2312,7 @@ testSwitchAsync :: HasCallStack => InitialAgentServers -> IO () testSwitchAsync servers = do (aId, bId) <- withA $ \a -> withB $ \b -> runRight $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId pure (aId, bId) let withA' = sessionSubscribe withA [bId] withB' = sessionSubscribe withB [aId] @@ -2286,8 +2333,8 @@ testSwitchAsync servers = do withA $ \a -> withB $ \b -> runRight_ $ do subscribeConnection a bId subscribeConnection b aId - exchangeGreetingsMsgId 10 a bId b aId - testFullSwitch a bId b aId 16 + exchangeGreetingsMsgId 8 a bId b aId + testFullSwitch a bId b aId 14 where withA :: (AgentClient -> IO a) -> IO a withA = withAgent 1 agentCfg servers testDB @@ -2310,7 +2357,7 @@ testSwitchDelete :: InitialAgentServers -> IO () testSwitchDelete servers = withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId liftIO $ disposeAgentClient b stats <- switchConnectionAsync a "" bId liftIO $ rcvSwchStatuses' stats `shouldMatchList` [Just RSSwitchStarted] @@ -2325,7 +2372,7 @@ testAbortSwitchStarted :: HasCallStack => InitialAgentServers -> IO () testAbortSwitchStarted servers = do (aId, bId) <- withA $ \a -> withB $ \b -> runRight $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId pure (aId, bId) let withA' = sessionSubscribe withA [bId] withB' = sessionSubscribe withB [aId] @@ -2362,9 +2409,9 @@ testAbortSwitchStarted servers = do phaseRcv a bId SPCompleted [Nothing] - exchangeGreetingsMsgId 12 a bId b aId + exchangeGreetingsMsgId 10 a bId b aId - testFullSwitch a bId b aId 18 + testFullSwitch a bId b aId 16 where withA :: (AgentClient -> IO a) -> IO a withA = withAgent 1 agentCfg servers testDB @@ -2375,7 +2422,7 @@ testAbortSwitchStartedReinitiate :: HasCallStack => InitialAgentServers -> IO () testAbortSwitchStartedReinitiate servers = do (aId, bId) <- withA $ \a -> withB $ \b -> runRight $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId pure (aId, bId) let withA' = sessionSubscribe withA [bId] withB' = sessionSubscribe withB [aId] @@ -2413,9 +2460,9 @@ testAbortSwitchStartedReinitiate servers = do phaseRcv a bId SPCompleted [Nothing] - exchangeGreetingsMsgId 12 a bId b aId + exchangeGreetingsMsgId 10 a bId b aId - testFullSwitch a bId b aId 18 + testFullSwitch a bId b aId 16 where withA :: (AgentClient -> IO a) -> IO a withA = withAgent 1 agentCfg servers testDB @@ -2442,7 +2489,7 @@ testCannotAbortSwitchSecured :: HasCallStack => InitialAgentServers -> IO () testCannotAbortSwitchSecured servers = do (aId, bId) <- withA $ \a -> withB $ \b -> runRight $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId pure (aId, bId) let withA' = sessionSubscribe withA [bId] withB' = sessionSubscribe withB [aId] @@ -2467,9 +2514,9 @@ testCannotAbortSwitchSecured servers = do phaseRcv a bId SPCompleted [Nothing] - exchangeGreetingsMsgId 10 a bId b aId + exchangeGreetingsMsgId 8 a bId b aId - testFullSwitch a bId b aId 16 + testFullSwitch a bId b aId 14 where withA :: (AgentClient -> IO a) -> IO a withA = withAgent 1 agentCfg servers testDB @@ -2480,9 +2527,9 @@ testSwitch2Connections :: HasCallStack => InitialAgentServers -> IO () testSwitch2Connections servers = do (aId1, bId1, aId2, bId2) <- withA $ \a -> withB $ \b -> runRight $ do (aId1, bId1) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId1 b aId1 + exchangeGreetings a bId1 b aId1 (aId2, bId2) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId2 b aId2 + exchangeGreetings a bId2 b aId2 pure (aId1, bId1, aId2, bId2) let withA' = sessionSubscribe withA [bId1, bId2] withB' = sessionSubscribe withB [aId1, aId2] @@ -2523,11 +2570,11 @@ testSwitch2Connections servers = do void $ subscribeConnections a [bId1, bId2] void $ subscribeConnections b [aId1, aId2] - exchangeGreetingsMsgId 10 a bId1 b aId1 - exchangeGreetingsMsgId 10 a bId2 b aId2 + exchangeGreetingsMsgId 8 a bId1 b aId1 + exchangeGreetingsMsgId 8 a bId2 b aId2 - testFullSwitch a bId1 b aId1 16 - testFullSwitch a bId2 b aId2 16 + testFullSwitch a bId1 b aId1 14 + testFullSwitch a bId2 b aId2 14 where withA :: (AgentClient -> IO a) -> IO a withA = withAgent 1 agentCfg servers testDB @@ -2538,9 +2585,9 @@ testSwitch2ConnectionsAbort1 :: HasCallStack => InitialAgentServers -> IO () testSwitch2ConnectionsAbort1 servers = do (aId1, bId1, aId2, bId2) <- withA $ \a -> withB $ \b -> runRight $ do (aId1, bId1) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId1 b aId1 + exchangeGreetings a bId1 b aId1 (aId2, bId2) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId2 b aId2 + exchangeGreetings a bId2 b aId2 pure (aId1, bId1, aId2, bId2) let withA' = sessionSubscribe withA [bId1, bId2] withB' = sessionSubscribe withB [aId1, aId2] @@ -2576,19 +2623,19 @@ testSwitch2ConnectionsAbort1 servers = do phaseRcv a bId1 SPCompleted [Nothing] - exchangeGreetingsMsgId 10 a bId1 b aId1 - exchangeGreetingsMsgId 8 a bId2 b aId2 + exchangeGreetingsMsgId 8 a bId1 b aId1 + exchangeGreetingsMsgId 6 a bId2 b aId2 - testFullSwitch a bId1 b aId1 16 - testFullSwitch a bId2 b aId2 14 + testFullSwitch a bId1 b aId1 14 + testFullSwitch a bId2 b aId2 12 where withA :: (AgentClient -> IO a) -> IO a withA = withAgent 1 agentCfg servers testDB withB :: (AgentClient -> IO a) -> IO a withB = withAgent 2 agentCfg servers testDB2 -testCreateQueueAuth :: HasCallStack => VersionSMP -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int -testCreateQueueAuth srvVersion clnt1 clnt2 = do +testCreateQueueAuth :: HasCallStack => VersionSMP -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> AgentMsgId -> IO Int +testCreateQueueAuth srvVersion clnt1 clnt2 baseId = do a <- getClient 1 clnt1 testDB b <- getClient 2 clnt2 testDB2 r <- runRight $ do @@ -2605,7 +2652,7 @@ testCreateQueueAuth srvVersion clnt1 clnt2 = do get a ##> ("", bId, CON) get b ##> ("", aId, INFO "alice's connInfo") get b ##> ("", aId, CON) - exchangeGreetings a bId b aId + exchangeGreetingsMsgId (baseId + 1) a bId b aId pure 2 disposeAgentClient a disposeAgentClient b @@ -2638,20 +2685,20 @@ testDeliveryReceipts = withAgentClients2 $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b -- a sends, b receives and sends delivery receipt - 4 <- sendMessage a bId SMP.noMsgFlags "hello" - get a ##> ("", bId, SENT 4) + 2 <- sendMessage a bId SMP.noMsgFlags "hello" + get a ##> ("", bId, SENT 2) get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False - ackMessage b aId 4 $ Just "" - get a =##> \case ("", c, Rcvd 4) -> c == bId; _ -> False - ackMessage a bId 5 Nothing + ackMessage b aId 2 $ Just "" + get a =##> \case ("", c, Rcvd 2) -> c == bId; _ -> False + ackMessage a bId 3 Nothing -- b sends, a receives and sends delivery receipt - 6 <- sendMessage b aId SMP.noMsgFlags "hello too" - get b ##> ("", aId, SENT 6) + 4 <- sendMessage b aId SMP.noMsgFlags "hello too" + get b ##> ("", aId, SENT 4) get a =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False - ackMessage a bId 6 $ Just "" - get b =##> \case ("", c, Rcvd 6) -> c == aId; _ -> False - ackMessage b aId 7 (Just "") `catchError` \case (A.CMD PROHIBITED _) -> pure (); e -> liftIO $ expectationFailure ("unexpected error " <> show e) - ackMessage b aId 7 Nothing + ackMessage a bId 4 $ Just "" + get b =##> \case ("", c, Rcvd 4) -> c == aId; _ -> False + ackMessage b aId 5 (Just "") `catchError` \case (A.CMD PROHIBITED _) -> pure (); e -> liftIO $ expectationFailure ("unexpected error " <> show e) + ackMessage b aId 5 Nothing testDeliveryReceiptsVersion :: HasCallStack => ATransport -> IO () testDeliveryReceiptsVersion t = do @@ -2662,15 +2709,15 @@ testDeliveryReceiptsVersion t = do (aId, bId) <- makeConnection_ PQSupportOff a b checkVersion a bId 3 checkVersion b aId 3 - (4, _) <- A.sendMessage a bId PQEncOff SMP.noMsgFlags "hello" - get a ##> ("", bId, SENT 4) - get b =##> \case ("", c, Msg' 4 PQEncOff "hello") -> c == aId; _ -> False - ackMessage b aId 4 $ Just "" + (2, _) <- A.sendMessage a bId PQEncOff SMP.noMsgFlags "hello" + get a ##> ("", bId, SENT 2) + get b =##> \case ("", c, Msg' 2 PQEncOff "hello") -> c == aId; _ -> False + ackMessage b aId 2 $ Just "" liftIO $ noMessages a "no delivery receipt (unsupported version)" - (5, _) <- A.sendMessage b aId PQEncOff SMP.noMsgFlags "hello too" - get b ##> ("", aId, SENT 5) - get a =##> \case ("", c, Msg' 5 PQEncOff "hello too") -> c == bId; _ -> False - ackMessage a bId 5 $ Just "" + (3, _) <- A.sendMessage b aId PQEncOff SMP.noMsgFlags "hello too" + get b ##> ("", aId, SENT 3) + get a =##> \case ("", c, Msg' 3 PQEncOff "hello too") -> c == bId; _ -> False + ackMessage a bId 3 $ Just "" liftIO $ noMessages b "no delivery receipt (unsupported version)" pure (aId, bId) @@ -2682,27 +2729,27 @@ testDeliveryReceiptsVersion t = do runRight_ $ do subscribeConnection a' bId subscribeConnection b' aId - exchangeGreetingsMsgId_ PQEncOff 6 a' bId b' aId - checkVersion a' bId 5 - checkVersion b' aId 5 - (8, PQEncOff) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello" - get a' ##> ("", bId, SENT 8) - get b' =##> \case ("", c, Msg' 8 PQEncOff "hello") -> c == aId; _ -> False - ackMessage b' aId 8 $ Just "" - get a' =##> \case ("", c, Rcvd 8) -> c == bId; _ -> False - ackMessage a' bId 9 Nothing - (10, PQEncOff) <- A.sendMessage b' aId PQEncOn SMP.noMsgFlags "hello too" - get b' ##> ("", aId, SENT 10) - get a' =##> \case ("", c, Msg' 10 PQEncOff "hello too") -> c == bId; _ -> False - ackMessage a' bId 10 $ Just "" - get b' =##> \case ("", c, Rcvd 10) -> c == aId; _ -> False - ackMessage b' aId 11 Nothing - (12, _) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello 2" - get a' ##> ("", bId, SENT 12) - get b' =##> \case ("", c, Msg' 12 PQEncOff "hello 2") -> c == aId; _ -> False - ackMessage b' aId 12 $ Just "" - get a' =##> \case ("", c, Rcvd 12) -> c == bId; _ -> False - ackMessage a' bId 13 Nothing + exchangeGreetingsMsgId_ PQEncOff 4 a' bId b' aId + checkVersion a' bId 6 + checkVersion b' aId 6 + (6, PQEncOff) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello" + get a' ##> ("", bId, SENT 6) + get b' =##> \case ("", c, Msg' 6 PQEncOff "hello") -> c == aId; _ -> False + ackMessage b' aId 6 $ Just "" + get a' =##> \case ("", c, Rcvd 6) -> c == bId; _ -> False + ackMessage a' bId 7 Nothing + (8, PQEncOff) <- A.sendMessage b' aId PQEncOn SMP.noMsgFlags "hello too" + get b' ##> ("", aId, SENT 8) + get a' =##> \case ("", c, Msg' 8 PQEncOff "hello too") -> c == bId; _ -> False + ackMessage a' bId 8 $ Just "" + get b' =##> \case ("", c, Rcvd 8) -> c == aId; _ -> False + ackMessage b' aId 9 Nothing + (10, _) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello 2" + get a' ##> ("", bId, SENT 10) + get b' =##> \case ("", c, Msg' 10 PQEncOff "hello 2") -> c == aId; _ -> False + ackMessage b' aId 10 $ Just "" + get a' =##> \case ("", c, Rcvd 10) -> c == bId; _ -> False + ackMessage a' bId 11 Nothing disposeAgentClient a' disposeAgentClient b' @@ -2773,8 +2820,8 @@ testTwoUsers = withAgentClients2 $ \a b -> do ("", "", UP _ _) <- nGet a a `hasClients` 2 - exchangeGreetingsMsgId 6 a bId1 b aId1 - exchangeGreetingsMsgId 6 a bId1' b aId1' + exchangeGreetingsMsgId 4 a bId1 b aId1 + exchangeGreetingsMsgId 4 a bId1' b aId1' liftIO $ threadDelay 250000 liftIO $ setNetworkConfig a nc {sessionMode = TSMUser} liftIO $ threadDelay 250000 @@ -2798,10 +2845,10 @@ testTwoUsers = withAgentClients2 $ \a b -> do ("", "", UP _ _) <- nGet a ("", "", UP _ _) <- nGet a a `hasClients` 4 - exchangeGreetingsMsgId 8 a bId1 b aId1 - exchangeGreetingsMsgId 8 a bId1' b aId1' - exchangeGreetingsMsgId 6 a bId2 b aId2 - exchangeGreetingsMsgId 6 a bId2' b aId2' + exchangeGreetingsMsgId 6 a bId1 b aId1 + exchangeGreetingsMsgId 6 a bId1' b aId1' + exchangeGreetingsMsgId 4 a bId2 b aId2 + exchangeGreetingsMsgId 4 a bId2' b aId2' liftIO $ threadDelay 250000 liftIO $ setNetworkConfig a nc {sessionMode = TSMUser} liftIO $ threadDelay 250000 @@ -2814,10 +2861,10 @@ testTwoUsers = withAgentClients2 $ \a b -> do ("", "", UP _ _) <- nGet a ("", "", UP _ _) <- nGet a a `hasClients` 2 - exchangeGreetingsMsgId 10 a bId1 b aId1 - exchangeGreetingsMsgId 10 a bId1' b aId1' - exchangeGreetingsMsgId 8 a bId2 b aId2 - exchangeGreetingsMsgId 8 a bId2' b aId2' + exchangeGreetingsMsgId 8 a bId1 b aId1 + exchangeGreetingsMsgId 8 a bId1' b aId1' + exchangeGreetingsMsgId 6 a bId2 b aId2 + exchangeGreetingsMsgId 6 a bId2' b aId2' where hasClients :: HasCallStack => AgentClient -> Int -> ExceptT AgentErrorType IO () hasClients c n = liftIO $ M.size <$> readTVarIO (smpClients c) `shouldReturn` n @@ -2846,7 +2893,7 @@ testServerMultipleIdentities = disposeAgentClient bob getSMPAgentClient' 3 agentCfg initAgentServers testDB2 subscribeConnection bob' aliceId - exchangeGreetingsMsgId 6 alice bobId bob' aliceId + exchangeGreetingsMsgId 4 alice bobId bob' aliceId liftIO $ disposeAgentClient bob' where secondIdentityCReq :: ConnectionRequestUri 'CMInvitation @@ -2934,7 +2981,7 @@ testServerQueueInfo = do aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice liftIO $ threadDelay 200000 - checkEmptyQ alice bobId False + checkEmptyQ alice bobId True -- secured by sender allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") @@ -2942,7 +2989,7 @@ testServerQueueInfo = do liftIO $ threadDelay 200000 checkEmptyQ alice bobId True checkEmptyQ bob aliceId True - let msgId = 4 + let msgId = 2 (msgId', PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello" liftIO $ msgId' `shouldBe` msgId get alice ##> ("", bobId, SENT msgId) @@ -3031,7 +3078,7 @@ exchangeGreetings :: HasCallStack => AgentClient -> ConnId -> AgentClient -> Con exchangeGreetings = exchangeGreetings_ PQEncOn exchangeGreetings_ :: HasCallStack => PQEncryption -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () -exchangeGreetings_ pqEnc = exchangeGreetingsMsgId_ pqEnc 4 +exchangeGreetings_ pqEnc = exchangeGreetingsMsgId_ pqEnc 2 exchangeGreetingsMsgId :: HasCallStack => Int64 -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetingsMsgId = exchangeGreetingsMsgId_ PQEncOn diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 01eab9555..ab9d8453c 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -12,9 +12,9 @@ module AgentTests.NotificationTests where -- import Control.Logger.Simple (LogConfig (..), LogLevel (..), setLogLevel, withGlobalLogging) import AgentTests.FunctionalAPITests - ( agentCfgV7, + ( agentCfgVPrevPQ, createConnection, - exchangeGreetingsMsgId, + exchangeGreetings, get, joinConnection, makeConnection, @@ -51,7 +51,7 @@ import qualified Data.ByteString.Char8 as B import Data.Text.Encoding (encodeUtf8) import NtfClient import SMPAgentClient (agentCfg, initAgentServers, initAgentServers2, testDB, testDB2, testNtfServer, testNtfServer2) -import SMPClient (cfg, cfgV7, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn) +import SMPClient (cfg, cfgVPrev, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore') import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers) @@ -70,7 +70,6 @@ import Simplex.Messaging.Transport (ATransport) import System.Directory (doesFileExist, removeFile) import Test.Hspec import UnliftIO -import Util removeFileIfExists :: FilePath -> IO () removeFileIfExists filePath = do @@ -144,28 +143,26 @@ notificationTests t = do withNtfServerOn t ntfTestPort2 . withNtfServerThreadOn t ntfTestPort $ \ntf -> testNotificationsNewToken apns ntf -testNtfMatrix :: ATransport -> (APNSMockServer -> AgentClient -> AgentClient -> IO ()) -> Spec +testNtfMatrix :: HasCallStack => ATransport -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> Spec testNtfMatrix t runTest = do describe "next and current" $ do - it "next servers: SMP v7, NTF v2; next clients: v7/v2" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfgV7 agentCfgV7 runTest - it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest - it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest - skip "this case cannot be supported - see RFC" $ - it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ - runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest - -- servers can be migrated in any order - it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest - it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest - -- clients can be partially migrated - it "servers: next SMP v7, curr NTF v2; clients: next/curr" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfgV7 agentCfg runTest - it "servers: next SMP v7, curr NTF v2; clients: curr/new" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfgV7 runTest + it "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest + it "curr servers; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest + it "prev servers; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest + it "prev servers; curr clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest + -- servers can be upgraded in any order + it "servers: curr SMP, prev NTF; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest + it "servers: prev SMP, curr NTF; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest + -- one of two clients can be upgraded + it "servers: curr SMP, curr NTF; clients: curr/prev" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfg agentCfgVPrevPQ runTest + it "servers: curr SMP, curr NTF; clients: prev/curr" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfgVPrevPQ agentCfg runTest -runNtfTestCfg :: ATransport -> ServerConfig -> NtfServerConfig -> AgentConfig -> AgentConfig -> (APNSMockServer -> AgentClient -> AgentClient -> IO ()) -> IO () -runNtfTestCfg t smpCfg ntfCfg aCfg bCfg runTest = do +runNtfTestCfg :: HasCallStack => ATransport -> AgentMsgId -> ServerConfig -> NtfServerConfig -> AgentConfig -> AgentConfig -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> IO () +runNtfTestCfg t baseId smpCfg ntfCfg aCfg bCfg runTest = do withSmpServerConfigOn t smpCfg testPort $ \_ -> withAPNSMockServer $ \apns -> withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t)]} $ \_ -> - withAgentClientsCfg2 aCfg bCfg $ runTest apns + withAgentClientsCfg2 aCfg bCfg $ runTest apns baseId threadDelay 100000 testNotificationToken :: APNSMockServer -> IO () @@ -345,8 +342,8 @@ testRunNTFServerTests t srv = withAgent 1 agentCfg initAgentServers testDB $ \a -> testProtocolServer a 1 $ ProtoServerWithAuth srv Nothing -testNotificationSubscriptionExistingConnection :: APNSMockServer -> AgentClient -> AgentClient -> IO () -testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@AgentClient {agentEnv = Env {config = aliceCfg, store}} bob = do +testNotificationSubscriptionExistingConnection :: APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO () +testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} baseId alice@AgentClient {agentEnv = Env {config = aliceCfg, store}} bob = do (bobId, aliceId, nonce, message) <- runRight $ do -- establish connection (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe @@ -404,11 +401,10 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@Agen -- no notifications should follow noNotification apnsQ where - baseId = 3 msgId = subtract baseId -testNotificationSubscriptionNewConnection :: APNSMockServer -> AgentClient -> AgentClient -> IO () -testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} alice bob = +testNotificationSubscriptionNewConnection :: HasCallStack => APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO () +testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} baseId alice bob = runRight_ $ do -- alice registers notification token DeviceToken {} <- registerTestToken alice "abcd" NMInstant apnsQ @@ -426,9 +422,9 @@ testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} alice bob = allowConnection alice bobId confId "alice's connInfo" void $ messageNotificationData bob apnsQ get bob ##> ("", aliceId, INFO "alice's connInfo") - void $ messageNotificationData alice apnsQ + when (baseId == 3) $ void $ messageNotificationData alice apnsQ get alice ##> ("", bobId, CON) - void $ messageNotificationData bob apnsQ + when (baseId == 3) $ void $ messageNotificationData bob apnsQ get bob ##> ("", aliceId, CON) -- bob sends message 1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello" @@ -445,7 +441,6 @@ testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} alice bob = -- no unexpected notifications should follow noNotification apnsQ where - baseId = 3 msgId = subtract baseId registerTestToken :: AgentClient -> ByteString -> NotificationsMode -> TBQueue APNSMockRequest -> ExceptT AgentErrorType IO DeviceToken @@ -520,7 +515,7 @@ testChangeNotificationsMode APNSMockServer {apnsQ} = -- no notifications should follow noNotification apnsQ where - baseId = 3 + baseId = 1 msgId = subtract baseId testChangeToken :: APNSMockServer -> IO () @@ -559,7 +554,7 @@ testChangeToken APNSMockServer {apnsQ} = withAgent 1 agentCfg initAgentServers t -- no notifications should follow noNotification apnsQ where - baseId = 3 + baseId = 1 msgId = subtract baseId testNotificationsStoreLog :: ATransport -> APNSMockServer -> IO () @@ -568,11 +563,11 @@ testNotificationsStoreLog t APNSMockServer {apnsQ} = withAgentClients2 $ \alice (aliceId, bobId) <- makeConnection alice bob _ <- registerTestToken alice "abcd" NMInstant apnsQ liftIO $ threadDelay 250000 - 4 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello" - get bob ##> ("", aliceId, SENT 4) + 2 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello" + get bob ##> ("", aliceId, SENT 2) void $ messageNotificationData alice apnsQ get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False - ackMessage alice bobId 4 Nothing + ackMessage alice bobId 2 Nothing liftIO $ killThread threadId pure (aliceId, bobId) @@ -580,8 +575,8 @@ testNotificationsStoreLog t APNSMockServer {apnsQ} = withAgentClients2 $ \alice withNtfServerStoreLog t $ \threadId -> runRight_ $ do liftIO $ threadDelay 250000 - 5 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again" - get bob ##> ("", aliceId, SENT 5) + 3 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again" + get bob ##> ("", aliceId, SENT 3) void $ messageNotificationData alice apnsQ get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False liftIO $ killThread threadId @@ -592,11 +587,11 @@ testNotificationsSMPRestart t APNSMockServer {apnsQ} = withAgentClients2 $ \alic (aliceId, bobId) <- makeConnection alice bob _ <- registerTestToken alice "abcd" NMInstant apnsQ liftIO $ threadDelay 250000 - 4 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello" - get bob ##> ("", aliceId, SENT 4) + 2 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello" + get bob ##> ("", aliceId, SENT 2) void $ messageNotificationData alice apnsQ get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False - ackMessage alice bobId 4 Nothing + ackMessage alice bobId 2 Nothing liftIO $ killThread threadId pure (aliceId, bobId) @@ -608,8 +603,8 @@ testNotificationsSMPRestart t APNSMockServer {apnsQ} = withAgentClients2 $ \alic nGet alice =##> \case ("", "", UP _ [c]) -> c == bobId; _ -> False nGet bob =##> \case ("", "", UP _ [c]) -> c == aliceId; _ -> False liftIO $ threadDelay 1000000 - 5 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again" - get bob ##> ("", aliceId, SENT 5) + 3 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again" + get bob ##> ("", aliceId, SENT 3) _ <- messageNotificationData alice apnsQ get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False liftIO $ killThread threadId @@ -664,7 +659,7 @@ testSwitchNotifications :: InitialAgentServers -> APNSMockServer -> IO () testSwitchNotifications servers APNSMockServer {apnsQ} = withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b - exchangeGreetingsMsgId 4 a bId b aId + exchangeGreetings a bId b aId _ <- registerTestToken a "abcd" NMInstant apnsQ liftIO $ threadDelay 250000 let testMessage msg = do @@ -739,7 +734,7 @@ messageNotification apnsQ = do pure (nonce, message) _ -> error "bad notification" -messageNotificationData :: AgentClient -> TBQueue APNSMockRequest -> ExceptT AgentErrorType IO PNMessageData +messageNotificationData :: HasCallStack => AgentClient -> TBQueue APNSMockRequest -> ExceptT AgentErrorType IO PNMessageData messageNotificationData c apnsQ = do (nonce, message) <- messageNotification apnsQ NtfToken {ntfDhSecret = Just dhSecret} <- getNtfTokenData c diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 039e26090..39a4b1b95 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -197,6 +197,9 @@ cData1 = testPrivateAuthKey :: C.APrivateAuthKey testPrivateAuthKey = C.APrivateAuthKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe" +testPublicAuthKey :: C.APublicAuthKey +testPublicAuthKey = C.APublicAuthKey C.SEd25519 (C.publicKey "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe") + testPrivDhKey :: C.PrivateKeyX25519 testPrivDhKey = "MC4CAQAwBQYDK2VuBCIEINCzbVFaCiYHoYncxNY8tSIfn0pXcIAhLBfFc0m+gOpk" @@ -218,6 +221,7 @@ rcvQueue1 = e2ePrivKey = testPrivDhKey, e2eDhSecret = Nothing, sndId = "2345", + sndSecure = True, status = New, dbQueueId = DBNewQueue, primary = True, @@ -235,7 +239,8 @@ sndQueue1 = connId = "conn1", server = smpServer1, sndId = "3456", - sndPublicKey = Nothing, + sndSecure = True, + sndPublicKey = testPublicAuthKey, sndPrivateKey = testPrivateAuthKey, e2ePubKey = Nothing, e2eDhSecret = testDhSecret, @@ -379,7 +384,8 @@ testUpgradeRcvConnToDuplex = connId = "conn1", server = SMPServer "smp.simplex.im" "5223" testKeyHash, sndId = "2345", - sndPublicKey = Nothing, + sndSecure = True, + sndPublicKey = testPublicAuthKey, sndPrivateKey = testPrivateAuthKey, e2ePubKey = Nothing, e2eDhSecret = testDhSecret, @@ -412,6 +418,7 @@ testUpgradeSndConnToDuplex = e2ePrivKey = testPrivDhKey, e2eDhSecret = Nothing, sndId = "4567", + sndSecure = True, status = New, dbQueueId = DBNewQueue, rcvSwchStatus = Nothing, diff --git a/tests/CoreTests/TRcvQueuesTests.hs b/tests/CoreTests/TRcvQueuesTests.hs index 2b0009344..9f7c4932e 100644 --- a/tests/CoreTests/TRcvQueuesTests.hs +++ b/tests/CoreTests/TRcvQueuesTests.hs @@ -183,6 +183,7 @@ dummyRQ userId server connId = e2ePrivKey = "MC4CAQAwBQYDK2VuBCIEINCzbVFaCiYHoYncxNY8tSIfn0pXcIAhLBfFc0m+gOpk", e2eDhSecret = Nothing, sndId = "", + sndSecure = True, status = New, dbQueueId = DBQueueId 0, primary = True, diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index bd8cee771..9bd124e55 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -28,7 +28,7 @@ import Network.HTTP.Types (Status) import qualified Network.HTTP.Types as N import qualified Network.HTTP2.Server as H import Network.Socket -import SMPClient (serverBracket) +import SMPClient (prevRange, serverBracket) import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultNetworkConfig) import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C @@ -36,7 +36,6 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Notifications.Protocol (NtfResponse) import Simplex.Messaging.Notifications.Server (runNtfServerBlocking) import Simplex.Messaging.Notifications.Server.Env -import qualified Simplex.Messaging.Notifications.Server.Env as Env import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Server.Push.APNS.Internal import Simplex.Messaging.Notifications.Transport @@ -47,7 +46,6 @@ import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), http2TLSParams) import Simplex.Messaging.Transport.HTTP2.Server import Simplex.Messaging.Transport.Server import qualified Simplex.Messaging.Transport.Server as Server -import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec import UnliftIO.Async import UnliftIO.Concurrent @@ -108,18 +106,19 @@ ntfServerCfg = serverStatsLogFile = "tests/ntf-server-stats.daily.log", serverStatsBackupFile = Nothing, ntfServerVRange = supportedServerNTFVRange, - transportConfig = defaultTransportServerConfig + transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedNTFHandshakes} } -ntfServerCfgV2 :: NtfServerConfig -ntfServerCfgV2 = +ntfServerCfgVPrev :: NtfServerConfig +ntfServerCfgVPrev = ntfServerCfg - { ntfServerVRange = mkVersionRange initialNTFVersion authBatchCmdsNTFVersion, - smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}}, - Env.transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedNTFHandshakes} + { ntfServerVRange = prevRange $ ntfServerVRange ntfServerCfg, + smpAgentCfg = smpAgentCfg' {smpCfg = smpCfg' {serverVRange = prevRange serverVRange'}} } where smpAgentCfg' = smpAgentCfg ntfServerCfg + smpCfg' = smpCfg smpAgentCfg' + serverVRange' = serverVRange smpCfg' withNtfServerStoreLog :: ATransport -> (ThreadId -> IO a) -> IO a withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t)]} diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 3c9907c48..7cb2a88c5 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -14,7 +14,7 @@ import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import NtfClient (ntfTestPort) -import SMPClient (proxyVRange, testPort) +import SMPClient (proxyVRangeV8, testPort) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval @@ -80,8 +80,8 @@ agentCfg = where networkConfig = defaultNetworkConfig {tcpConnectTimeout = 1_000_000, tcpTimeout = 2_000_000} -agentProxyCfg :: AgentConfig -agentProxyCfg = agentCfg {smpCfg = (smpCfg agentCfg) {serverVRange = proxyVRange}} +agentProxyCfgV8 :: AgentConfig +agentProxyCfgV8 = agentCfg {smpCfg = (smpCfg agentCfg) {serverVRange = proxyVRangeV8}} fastRetryInterval :: RetryInterval fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 6bc36c29a..144ad8b10 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -29,6 +29,7 @@ import qualified Simplex.Messaging.Transport.Client as Client import Simplex.Messaging.Transport.Server import qualified Simplex.Messaging.Transport.Server as Server import Simplex.Messaging.Version +import Simplex.Messaging.Version.Internal import System.Environment (lookupEnv) import System.Info (os) import Test.Hspec @@ -133,18 +134,26 @@ cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVer cfgV8 :: ServerConfig cfgV8 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} +cfgVPrev :: ServerConfig +cfgVPrev = cfg {smpServerVRange = prevRange $ smpServerVRange cfg} + +prevRange :: VersionRange v -> VersionRange v +prevRange vr = vr {maxVersion = max (minVersion vr) (prevVersion $ maxVersion vr)} + +prevVersion :: Version v -> Version v +prevVersion (Version v) = Version (v - 1) + proxyCfg :: ServerConfig proxyCfg = - cfgV7 + cfg { allowSMPProxy = True, - smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, - smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {serverVRange = proxyVRange, agreeSecret = True}} + smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {agreeSecret = True}} } where - smpAgentCfg' = smpAgentCfg cfgV7 + smpAgentCfg' = smpAgentCfg cfg -proxyVRange :: VersionRangeSMP -proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion +proxyVRangeV8 :: VersionRangeSMP +proxyVRangeV8 = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} @@ -180,9 +189,6 @@ withSmpServerOn t port' = withSmpServerThreadOn t port' . const withSmpServer :: HasCallStack => ATransport -> IO a -> IO a withSmpServer t = withSmpServerOn t testPort -withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a -withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const - withSmpServerProxy :: HasCallStack => ATransport -> IO a -> IO a withSmpServerProxy t = withSmpServerConfigOn t proxyCfg testPort . const diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index d71208db9..6452d2677 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -102,22 +102,22 @@ smpProxyTests = do describe "agent API" $ do describe "one server" $ do it "always via proxy" . oneServer $ - agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1 it "without proxy" . oneServer $ - agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 describe "two servers" $ do it "always via proxy" . twoServers $ - agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1 it "both via proxy" . twoServers $ - agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" 1 it "first via proxy" . twoServers $ - agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 it "without proxy" . twoServers $ - agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 it "first via proxy for unknown" . twoServers $ - agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 1 it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $ - agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 3 it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ agentViaProxyVersionError it "retries sending when destination or proxy relay is offline" $ @@ -157,7 +157,7 @@ deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do -- prepare receiving queue (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe + QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe False let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv -- get proxy session sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") @@ -199,8 +199,8 @@ proxyConnectDeadRelay n d proxyServ = do Right !_noWay -> error "got unexpected client" Left !_err -> threadDelay d -agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> IO () -agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 = +agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> AgentMsgId -> IO () +agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 baseId = withAgent 1 aCfg (servers aTestCfg) testDB $ \alice -> withAgent 2 aCfg (servers bTestCfg) testDB2 $ \bob -> runRight_ $ do (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe @@ -232,9 +232,8 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False ackMessage alice bobId (baseId + 4) Nothing where - baseId = 3 msgId = subtract baseId . fst - aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} + aCfg = agentCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} agentDeliverMessagesViaProxyConc :: [NonEmpty SMPServer] -> [MsgBody] -> IO () @@ -299,14 +298,14 @@ agentDeliverMessagesViaProxyConc agentServers msgs = Left (Left e) -> cancel aSender >> throwIO e logDebug "run finished" pqEnc = CR.PQEncOn - aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg C.SEd448, rcvAuthAlg = C.AuthAlg C.SEd448} + aCfg = agentCfg {sndAuthAlg = C.AuthAlg C.SEd448, rcvAuthAlg = C.AuthAlg C.SEd448} servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} agentViaProxyVersionError :: IO () agentViaProxyVersionError = - withAgent 1 agentProxyCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do + withAgent 1 agentCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do Left (A.BROKER _ (TRANSPORT TEVersion)) <- - withAgent 2 agentProxyCfg (servers [SMPServer testHost testPort2 testKeyHash]) testDB2 $ \bob -> runExceptT $ do + withAgent 2 agentCfg (servers [SMPServer testHost testPort2 testKeyHash]) testDB2 $ \bob -> runExceptT $ do (_bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe pure () @@ -370,22 +369,22 @@ agentViaProxyRetryOffline = do withSmpServerConfigOn (transport @TLS) proxyCfg {storeLogFile = Just storeLog, storeMsgsFile = Just storeMsgs} port a `up` cId = nGet a =##> \case ("", "", UP _ [c]) -> c == cId; _ -> False a `down` cId = nGet a =##> \case ("", "", DOWN _ [c]) -> c == cId; _ -> False - aCfg = agentProxyCfg {messageRetryInterval = fastMessageRetryInterval} - baseId = 3 + aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval} + baseId = 1 msgId = subtract baseId . fst servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]} testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do - testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do + testSMPClient_ "127.0.0.1" testPort2 proxyVRangeV8 $ \(th :: THandleSMP TLS 'TClient) -> do (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do - testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do + testSMPClient_ "127.0.0.1" testPort proxyVRangeV8 $ \(th :: THandleSMP TLS 'TClient) -> do (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) where diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index a124a42e4..1fa76dfaa 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -73,7 +73,7 @@ pattern Resp :: CorrId -> QueueId -> BrokerMsg -> SignedTransmission ErrorType B pattern Resp corrId queueId command <- (_, _, (corrId, queueId, Right command)) pattern Ids :: RecipientId -> SenderId -> RcvPublicDhKey -> BrokerMsg -pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh) +pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure) pattern Msg :: MsgId -> MsgBody -> BrokerMsg pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} @@ -134,7 +134,7 @@ testCreateSecure (ATransport t) = g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv r rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv r rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv (rId1, "") #== "creates queue" @@ -199,7 +199,7 @@ testCreateDelete (ATransport t) = g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv (rId1, "") #== "creates queue" @@ -271,7 +271,7 @@ stressTest (ATransport t) = (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g (dhPub, _ :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g rIds <- forM ([1 .. 50] :: [Int]) . const $ do - Resp "" "" (Ids rId _ _) <- signSendRecv h1 rKey ("", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "" "" (Ids rId _ _) <- signSendRecv h1 rKey ("", "", NEW rPub dhPub Nothing SMSubscribe False) pure rId let subscribeQueues h = forM_ rIds $ \rId -> do Resp "" rId' OK <- signSendRecv h rKey ("", rId, SUB) @@ -289,7 +289,7 @@ testAllowNewQueues t = g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (dhPub, _ :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" "" (ERR AUTH) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "abcd" "" (ERR AUTH) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False) pure () testDuplex :: ATransport -> Spec @@ -299,7 +299,7 @@ testDuplex (ATransport t) = g <- C.newRandom (arPub, arKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (aDhPub, aDhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", "", NEW arPub aDhPub Nothing SMSubscribe) + Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", "", NEW arPub aDhPub Nothing SMSubscribe False) let aDec = decryptMsgV3 $ C.dh' aSrvDh aDhPriv -- aSnd ID is passed to Bob out-of-band @@ -315,7 +315,7 @@ testDuplex (ATransport t) = (brPub, brKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (bDhPub, bDhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub Nothing SMSubscribe) + Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub Nothing SMSubscribe False) let bDec = decryptMsgV3 $ C.dh' bSrvDh bDhPriv Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, _SEND $ "reply_id " <> encode bSnd) -- "reply_id ..." is ad-hoc, not a part of SMP protocol @@ -354,7 +354,7 @@ testSwitchSub (ATransport t) = g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, _SEND "test1") (ok1, OK) #== "sent test message 1" @@ -740,7 +740,7 @@ createAndSecureQueue h sPub = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False) let dhShared = C.dh' srvDh dhPriv Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub) (rId', rId) #== "same queue ID" @@ -751,7 +751,7 @@ testTiming (ATransport t) = describe "should have similar time for auth error, whether queue exists or not, for all key types" $ forM_ timingTests $ \tst -> it (testName tst) $ - smpTest2Cfg cfgV7 (mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion) t $ \rh sh -> + smpTest2Cfg cfg (mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion) t $ \rh sh -> testSameTiming rh sh tst where testName :: (C.AuthAlg, C.AuthAlg, Int) -> String @@ -775,7 +775,7 @@ testTiming (ATransport t) = g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) + Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv Resp "cdab" _ OK <- signSendRecv rh rKey ("cdab", rId, SUB) @@ -937,8 +937,8 @@ syntaxTests (ATransport t) = do describe "NEW" $ do it "no parameters" $ (sampleSig, "bcda", "", NEW_) >#> ("", "bcda", "", ERR $ CMD SYNTAX) it "many parameters" $ (sampleSig, "cdab", "", (NEW_, ' ', ('\x01', 'A'), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX) - it "no signature" $ ("", "dabc", "", (NEW_, ' ', samplePubKey, sampleDhPubKey, SMSubscribe)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH) - it "queue ID" $ (sampleSig, "abcd", "12345678", (NEW_, ' ', samplePubKey, sampleDhPubKey, SMSubscribe)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH) + it "no signature" $ ("", "dabc", "", (NEW_, ' ', samplePubKey, sampleDhPubKey, '0', SMSubscribe, False)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH) + it "queue ID" $ (sampleSig, "abcd", "12345678", (NEW_, ' ', samplePubKey, sampleDhPubKey, '0', SMSubscribe, False)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH) describe "KEY" $ do it "valid syntax" $ (sampleSig, "bcda", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "12345678", ERR AUTH) it "no parameters" $ (sampleSig, "cdab", "12345678", KEY_) >#> ("", "cdab", "12345678", ERR $ CMD SYNTAX) diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 37ec00199..8de86eff1 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -398,7 +398,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do -- receive file - should fail with AUTH error withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do runRight_ $ xftpStartWorkers rcp' (Just recipientFiles) - ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp' + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" AUTH)) <- rfGet rcp' rfId' `shouldBe` rfId -- tmp path should be removed after permanent error @@ -477,7 +477,7 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do -- send file - should fail with AUTH error withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do runRight_ $ xftpStartWorkers sndr' (Just senderFiles) - ("", sfId', SFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + ("", sfId', SFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" AUTH)) <- sfGet sndr' sfId' `shouldBe` sfId @@ -513,7 +513,7 @@ testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $ withAgent 3 agentCfg initAgentServers testDB2 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True - ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" AUTH)) <- rfGet rcp2 liftIO $ rfId' `shouldBe` rfId @@ -551,7 +551,7 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do withAgent 5 agentCfg initAgentServers testDB3 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True - ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" AUTH)) <- rfGet rcp2 liftIO $ rfId' `shouldBe` rfId @@ -586,7 +586,7 @@ testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $ runRight_ . void $ do -- receive file 1 again rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing True - ("", rfId1', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + ("", rfId1', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" AUTH)) <- rfGet rcp liftIO $ rfId1 `shouldBe` rfId1' @@ -619,7 +619,7 @@ testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do -- receive file 1 again - should fail with AUTH error runRight $ do rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing True - ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" AUTH)) <- rfGet rcp liftIO $ rfId' `shouldBe` rfId diff --git a/tests/XFTPClient.hs b/tests/XFTPClient.hs index 208b54dc5..72c843f32 100644 --- a/tests/XFTPClient.hs +++ b/tests/XFTPClient.hs @@ -67,10 +67,10 @@ withXFTPServer2 :: HasCallStack => IO a -> IO a withXFTPServer2 = withXFTPServerCfg testXFTPServerConfig {xftpPort = xftpTestPort2, filesPath = xftpServerFiles2} . const xftpTestPort :: ServiceName -xftpTestPort = "7000" +xftpTestPort = "8000" xftpTestPort2 :: ServiceName -xftpTestPort2 = "7001" +xftpTestPort2 = "8001" testXFTPServer :: XFTPServer testXFTPServer = fromString testXFTPServerStr @@ -79,10 +79,10 @@ testXFTPServer2 :: XFTPServer testXFTPServer2 = fromString testXFTPServerStr2 testXFTPServerStr :: String -testXFTPServerStr = "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" +testXFTPServerStr = "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8000" testXFTPServerStr2 :: String -testXFTPServerStr2 = "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001" +testXFTPServerStr2 = "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:8001" xftpServerFiles :: FilePath xftpServerFiles = "tests/tmp/xftp-server-files" From 6a54a58a0d47aeca9d5482b0d27e451731b692f1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Jun 2024 12:50:42 +0100 Subject: [PATCH 104/125] agent: remove legacy statistics (#1211) * agent: remove legacy statistics * delays after disposeAgent * delays * enable all tests * more delays --- src/Simplex/FileTransfer/Client.hs | 4 - src/Simplex/Messaging/Agent.hs | 31 +------ src/Simplex/Messaging/Agent/Client.hs | 116 ++++++------------------- tests/AgentTests/FunctionalAPITests.hs | 11 ++- 4 files changed, 36 insertions(+), 126 deletions(-) diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 445def724..1404fd434 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -22,7 +22,6 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty (..)) -import Data.Time (UTCTime) import Data.Word (Word32) import qualified Data.X509 as X import qualified Data.X509.Validation as XV @@ -168,9 +167,6 @@ xftpClientServer = B.unpack . strEncode . snd3 . transportSession xftpTransportHost :: XFTPClient -> TransportHost xftpTransportHost XFTPClient {http2Client = HTTP2Client {client_ = HClient {host}}} = host -xftpSessionTs :: XFTPClient -> UTCTime -xftpSessionTs = sessionTs . http2Client - xftpHTTP2Config :: TransportClientConfig -> XFTPClientConfig -> HTTP2ClientConfig xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} = defaultHTTP2ClientConfig diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 0c433cfed..c4f62702e 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -111,9 +111,6 @@ module Simplex.Messaging.Agent execAgentStoreSQL, getAgentMigrations, debugAgentLocks, - getAgentStats, - resetAgentStats, - getMsgCounts, getAgentSubscriptions, logConnection, ) @@ -126,7 +123,7 @@ import Control.Monad.Reader import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J -import Data.Bifunctor (bimap, first, second) +import Data.Bifunctor (bimap, first) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Composition ((.:), (.:.), (.::), (.::.)) @@ -591,16 +588,6 @@ resetAgentServersStats :: AgentClient -> AE () resetAgentServersStats c = withAgentEnv c $ resetAgentServersStats' c {-# INLINE resetAgentServersStats #-} -getAgentStats :: AgentClient -> IO [(AgentStatsKey, Int)] -getAgentStats c = readTVarIO (agentStats c) >>= mapM (\(k, cnt) -> (k,) <$> readTVarIO cnt) . M.assocs - -resetAgentStats :: AgentClient -> IO () -resetAgentStats = atomically . TM.clear . agentStats -{-# INLINE resetAgentStats #-} - -getMsgCounts :: AgentClient -> IO [(ConnId, (Int, Int))] -- (total, duplicates) -getMsgCounts c = readTVarIO (msgCounts c) >>= mapM (\(connId, cnt) -> (connId,) <$> readTVarIO cnt) . M.assocs - withAgentEnv' :: AgentClient -> AM' a -> IO a withAgentEnv' c = (`runReaderT` agentEnv c) {-# INLINE withAgentEnv' #-} @@ -2246,7 +2233,6 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) _ -> pure () let encryptedMsgHash = C.sha256Hash encAgentMessage g <- asks random - atomically updateTotalMsgCount tryAgentError (agentClientMsg g encryptedMsgHash) >>= \case Right (Just (msgId, msgMeta, aMessage, rcPrev)) -> do conn'' <- resetRatchetSync @@ -2280,7 +2266,6 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) | otherwise = pure conn' Right Nothing -> prohibited "msg: bad agent msg" >> ack Left e@(AGENT A_DUPLICATE) -> do - atomically updateDupMsgCount atomically $ incSMPServerStat c userId srv recvDuplicates withStore' c (\db -> getLastMsg db connId srvMsgId) >>= \case Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck} @@ -2315,20 +2300,6 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) checkDuplicateHash e encryptedMsgHash = unlessM (withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash) $ throwE e - updateTotalMsgCount :: STM () - updateTotalMsgCount = - TM.lookup connId (msgCounts c) >>= \case - Just v -> modifyTVar' v $ first (+ 1) - Nothing -> addMsgCount 0 - updateDupMsgCount :: STM () - updateDupMsgCount = - TM.lookup connId (msgCounts c) >>= \case - Just v -> modifyTVar' v $ second (+ 1) - Nothing -> addMsgCount 1 - addMsgCount :: Int -> STM () - addMsgCount duplicate = do - counts <- newTVar (1, duplicate) - TM.insert connId counts (msgCounts c) agentClientMsg :: TVar ChaChaDRG -> ByteString -> AM (Maybe (InternalId, MsgMeta, AMessage, CR.RatchetX448)) agentClientMsg g encryptedMsgHash = withStore c $ \db -> runExceptT $ do rc <- ExceptT $ getRatchet db connId -- ratchet state pre-decryption - required for processing EREADY diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index ec8424745..7f5bb43e6 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -104,7 +104,6 @@ module Simplex.Messaging.Agent.Client AgentOpState (..), AgentState (..), AgentLocks (..), - AgentStatsKey (..), getAgentWorker, getAgentWorker', cancelWorker, @@ -158,7 +157,7 @@ module Simplex.Messaging.Agent.Client where import Control.Applicative ((<|>)) -import Control.Concurrent (ThreadId, forkIO, threadDelay) +import Control.Concurrent (ThreadId, forkIO) import Control.Concurrent.Async (Async, uninterruptibleCancel) import Control.Concurrent.STM (retry, throwSTM) import Control.Exception (AsyncException (..), BlockedIndefinitelyOnSTM (..)) @@ -321,8 +320,6 @@ data AgentClient = AgentClient deleteLock :: Lock, -- smpSubWorkers for SMP servers sessions smpSubWorkers :: TMap SMPTransportSession (SessionVar (Async ())), - agentStats :: TMap AgentStatsKey (TVar Int), - msgCounts :: TMap ConnId (TVar (Int, Int)), -- (total, duplicates) clientId :: Int, agentEnv :: Env, smpServersStats :: TMap (UserId, SMPServer) AgentSMPServerStats, @@ -430,15 +427,6 @@ data AgentLocks = AgentLocks } deriving (Show) -data AgentStatsKey = AgentStatsKey - { userId :: UserId, - host :: ByteString, - clientTs :: ByteString, - cmd :: ByteString, - res :: ByteString - } - deriving (Eq, Ord, Show) - data UserNetworkInfo = UserNetworkInfo { networkType :: UserNetworkType, online :: Bool @@ -492,8 +480,6 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs a invLocks <- TM.empty deleteLock <- createLock smpSubWorkers <- TM.empty - agentStats <- TM.empty - msgCounts <- TM.empty smpServersStats <- TM.empty xftpServersStats <- TM.empty srvStatsStartedAt <- newTVar currentTs @@ -532,8 +518,6 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs a invLocks, deleteLock, smpSubWorkers, - agentStats, - msgCounts, clientId, agentEnv, smpServersStats, @@ -565,7 +549,6 @@ class (Encoding err, Show err) => ProtocolServerClient v err msg | msg -> v, msg closeProtocolServerClient :: ProtoClient msg -> IO () clientServer :: ProtoClient msg -> String clientTransportHost :: ProtoClient msg -> TransportHost - clientSessionTs :: ProtoClient msg -> UTCTime instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where type Client BrokerMsg = SMPConnectedClient @@ -576,7 +559,6 @@ instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer clientTransportHost = transportHost' - clientSessionTs = sessionTs instance ProtocolServerClient NTFVersion ErrorType NtfResponse where type Client NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse @@ -587,7 +569,6 @@ instance ProtocolServerClient NTFVersion ErrorType NtfResponse where closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer clientTransportHost = transportHost' - clientSessionTs = sessionTs instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where type Client FileResponse = XFTPClient @@ -598,7 +579,6 @@ instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where closeProtocolServerClient = X.closeXFTPClient clientServer = X.xftpClientServer clientTransportHost = X.xftpTransportHost - clientSessionTs = X.xftpSessionTs getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do @@ -642,14 +622,12 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) pure (clnt, sess) newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) - newProxiedRelay clnt@(SMPConnectedClient smp prs) proxyAuth rv = + newProxiedRelay (SMPConnectedClient smp prs) proxyAuth rv = tryAgentError (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp destSrv proxyAuth) >>= \case Right sess -> do atomically $ putTMVar (sessionVar rv) (Right sess) - liftIO $ incClientStat c userId clnt "PROXY" "OK" pure $ Right sess Left e -> do - liftIO $ incClientStat c userId clnt "PROXY" $ bshow e atomically $ do unless (serverHostError e) $ do removeSessVar rv destSrv prs @@ -701,7 +679,6 @@ smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess serverDown :: ([RcvQueue], [ConnId]) -> IO () serverDown (qs, conns) = whenM (readTVarIO active) $ do - incClientStat' c userId client "DISCONNECT" "" notifySub "" $ hostEvent' DISCONNECT client unless (null conns) $ notifySub "" $ DOWN srv conns unless (null qs) $ do @@ -763,7 +740,7 @@ reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, AEvt (sAEntity @e) cmd) getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient -getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, srv, _) = do +getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(_, srv, _) = do unlessM (readTVarIO active) $ throwE INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess ntfClients ts) @@ -782,12 +759,11 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, clientDisconnected :: NtfClientVar -> NtfClient -> IO () clientDisconnected v client = do atomically $ removeSessVar v tSess ntfClients - incClientStat c userId client "DISCONNECT" "" atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient -getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId, srv, _) = do +getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(_, srv, _) = do unlessM (readTVarIO active) $ throwE INACTIVE ts <- liftIO getCurrentTime atomically (getSessVar workerSeq tSess xftpClients ts) @@ -806,7 +782,6 @@ getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId clientDisconnected :: XFTPClientVar -> XFTPClient -> IO () clientDisconnected v client = do atomically $ removeSessVar v tSess xftpClients - incClientStat c userId client "DISCONNECT" "" atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv @@ -846,11 +821,9 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = Right client -> do logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv <> " (user " <> bshow userId <> maybe "" (" for entity " <>) entityId_ <> ")" atomically $ putTMVar (sessionVar v) (Right client) - liftIO $ incClientStat c userId client "CLIENT" "OK" atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent CONNECT client) pure client Left e -> do - liftIO $ incServerStat c userId srv "CLIENT" $ bshow e ei <- asks $ persistErrorInterval . config if ei == 0 then atomically $ do @@ -1004,46 +977,42 @@ getMapLock locks key = TM.lookup key locks >>= maybe newLock pure where newLock = createLock >>= \l -> TM.insert key l locks $> l -withClient_ :: forall a v err msg. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> ByteString -> (Client msg -> AM a) -> AM a -withClient_ c tSess@(userId, srv, _) statCmd action = do +withClient_ :: forall a v err msg. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> (Client msg -> AM a) -> AM a +withClient_ c tSess@(_, srv, _) action = do cl <- getProtocolServerClient c tSess - (action cl <* stat cl "OK") `catchAgentError` logServerError cl + action cl `catchAgentError` logServerError where - stat cl = liftIO . incClientStat c userId cl statCmd - logServerError :: Client msg -> AgentErrorType -> AM a - logServerError cl e = do + logServerError :: AgentErrorType -> AM a + logServerError e = do logServer "<--" c srv "" $ bshow e - stat cl $ bshow e throwE e withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a -withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do +withProxySession c destSess@(_, destSrv, _) entId cmdStr action = do (cl, sess_) <- getSMPProxyClient c destSess logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr case sess_ of Right sess -> do - r <- (action (cl, sess) <* stat cl "OK") `catchAgentError` logServerError cl + r <- action (cl, sess) `catchAgentError` logServerError cl logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" pure r Left e -> logServerError cl e where - stat cl = liftIO . incClientStat c userId cl cmdStr proxySrv = showServer . protocolClientServer' . protocolClient logServerError :: SMPConnectedClient -> AgentErrorType -> AM a logServerError cl e = do logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ bshow e - stat cl $ bshow e throwE e withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> AM a) -> AM a withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do logServer "-->" c srv entId cmdStr - res <- withClient_ c tSess cmdStr action + res <- withClient_ c tSess action logServer "<--" c srv entId "OK" return res -withClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client +withClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a +withClient c tSess action = withClient_ c tSess $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withClient #-} withLogClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a @@ -1235,7 +1204,6 @@ runXFTPServerTest c userId (ProtoServerWithAuth srv auth) = do unless (digest == rcvDigest) $ throwE $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST liftError (testErr TSDeleteFile) $ X.deleteXFTPChunk xftp spKey sId ok <- tcpTimeout xftpNetworkConfig `timeout` X.closeXFTPClient xftp - incClientStat c userId xftp "XFTP_TEST" "OK" pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok Left e -> pure (Just $ testErr TSConnect e) where @@ -1274,7 +1242,6 @@ runNTFServerTest c userId (ProtoServerWithAuth srv _) = do (tknId, _) <- liftError (testErr TSCreateNtfToken) $ ntfRegisterToken ntf npKey (NewNtfTkn deviceToken nKey dhKey) liftError (testErr TSDeleteNtfToken) $ ntfDeleteToken ntf npKey tknId ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient ntf - incClientStat c userId ntf "NTF_TEST" "OK" pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok Left e -> pure (Just $ testErr TSConnect e) where @@ -1317,7 +1284,7 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do logServer "-->" c srv "" "NEW" tSess <- liftIO $ mkTransportSession c userId srv connId (sessId, QIK {rcvId, sndId, rcvPublicDhKey}) <- - withClient c tSess "NEW" $ \(SMPConnectedClient smp _) -> + withClient c tSess $ \(SMPConnectedClient smp _) -> (sessionId $ thParams smp,) <$> createSMPQueue smp rKeys dhKey auth subMode liftIO . logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = @@ -1393,7 +1360,7 @@ subscribeQueues c qs = do env <- ask -- only "checked" queues are subscribed session <- newTVarIO Nothing - rs <- sendTSessionBatches "SUB" 90 id (subscribeQueues_ env session) c qs' + rs <- sendTSessionBatches "SUB" id (subscribeQueues_ env session) c qs' (errs <> rs,) <$> readTVarIO session where checkQueue rq = do @@ -1432,9 +1399,8 @@ activeClientSession c tSess sessId = sameSess <$> tryReadSessVar tSess (smpClien type BatchResponses e r = NonEmpty (RcvQueue, Either e r) --- statBatchSize is not used to batch the commands, only for traffic statistics -sendTSessionBatches :: forall q r. ByteString -> Int -> (q -> RcvQueue) -> (SMPClient -> NonEmpty q -> IO (BatchResponses SMPClientError r)) -> AgentClient -> [q] -> AM' [(RcvQueue, Either AgentErrorType r)] -sendTSessionBatches statCmd statBatchSize toRQ action c qs = +sendTSessionBatches :: forall q r. ByteString -> (q -> RcvQueue) -> (SMPClient -> NonEmpty q -> IO (BatchResponses SMPClientError r)) -> AgentClient -> [q] -> AM' [(RcvQueue, Either AgentErrorType r)] +sendTSessionBatches statCmd toRQ action c qs = concatMap L.toList <$> (mapConcurrently sendClientBatch =<< batchQueues) where batchQueues :: AM' [(SMPTransportSession, NonEmpty q)] @@ -1446,19 +1412,14 @@ sendTSessionBatches statCmd statBatchSize toRQ action c qs = let tSess = mkSMPTSession (toRQ q) mode in M.alter (Just . maybe [q] (q <|)) tSess m sendClientBatch :: (SMPTransportSession, NonEmpty q) -> AM' (BatchResponses AgentErrorType r) - sendClientBatch (tSess@(userId, srv, _), qs') = + sendClientBatch (tSess@(_, srv, _), qs') = tryAgentError' (getSMPServerClient c tSess) >>= \case Left e -> pure $ L.map ((,Left e) . toRQ) qs' Right (SMPConnectedClient smp _) -> liftIO $ do logServer "-->" c srv (bshow (length qs') <> " queues") statCmd - rs <- L.map agentError <$> action smp qs' - statBatch - pure rs + L.map agentError <$> action smp qs' where agentError = second . first $ protocolClientError SMP $ clientServer smp - statBatch = - let n = (length qs - 1) `div` statBatchSize + 1 - in incClientStatN c userId smp n statCmd "OK" sendBatch :: (SMPClient -> NonEmpty (SMP.RcvPrivateAuthKey, SMP.RecipientId) -> IO (NonEmpty (Either SMPClientError ()))) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) sendBatch smpCmdFunc smp qs = L.zip qs <$> smpCmdFunc smp (L.map queueCreds qs) @@ -1574,7 +1535,7 @@ enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtf enableSMPQueueNotifications smp rcvPrivateKey rcvId notifierKey rcvNtfPublicDhKey enableQueuesNtfs :: AgentClient -> [(RcvQueue, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey)] -> AM' [(RcvQueue, Either AgentErrorType (SMP.NotifierId, SMP.RcvNtfPublicDhKey))] -enableQueuesNtfs = sendTSessionBatches "NKEY" 90 fst3 enableQueues_ +enableQueuesNtfs = sendTSessionBatches "NKEY" fst3 enableQueues_ where fst3 (x, _, _) = x enableQueues_ :: SMPClient -> NonEmpty (RcvQueue, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey) -> IO (NonEmpty (RcvQueue, Either (ProtocolClientError ErrorType) (SMP.NotifierId, RcvNtfPublicDhKey))) @@ -1588,7 +1549,7 @@ disableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} = disableSMPQueueNotifications smp rcvPrivateKey rcvId disableQueuesNtfs :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] -disableQueuesNtfs = sendTSessionBatches "NDEL" 90 id $ sendBatch disableSMPQueuesNtfs +disableQueuesNtfs = sendTSessionBatches "NDEL" id $ sendBatch disableSMPQueuesNtfs sendAck :: AgentClient -> RcvQueue -> MsgId -> AM () sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId = do @@ -1615,7 +1576,7 @@ deleteQueue c rq@RcvQueue {rcvId, rcvPrivateKey} = do deleteSMPQueue smp rcvPrivateKey rcvId deleteQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] -deleteQueues = sendTSessionBatches "DEL" 90 id $ sendBatch deleteSMPQueues +deleteQueues = sendTSessionBatches "DEL" id $ sendBatch deleteSMPQueues sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM (Maybe SMPServer) sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags agentMsg = do @@ -1630,7 +1591,7 @@ getQueueInfo c rq@RcvQueue {rcvId, rcvPrivateKey} = agentNtfRegisterToken :: AgentClient -> NtfToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519) agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey = - withClient c (0, ntfServer, Nothing) "TNEW" $ \ntf -> ntfRegisterToken ntf ntfPrivKey (NewNtfTkn deviceToken ntfPubKey pubDhKey) + withClient c (0, ntfServer, Nothing) $ \ntf -> ntfRegisterToken ntf ntfPrivKey (NewNtfTkn deviceToken ntfPubKey pubDhKey) agentNtfVerifyToken :: AgentClient -> NtfTokenId -> NtfToken -> NtfRegCode -> AM () agentNtfVerifyToken c tknId NtfToken {ntfServer, ntfPrivKey} code = @@ -1676,7 +1637,7 @@ agentXFTPNewChunk c SndFileChunk {userId, chunkSpec = XFTPChunkSpec {chunkSize}, let fileInfo = FileInfo {sndKey, size = chunkSize, digest = chunkDigest} logServer "-->" c srv "" "FNEW" tSess <- liftIO $ mkTransportSession c userId srv chunkDigest - (sndId, rIds) <- withClient c tSess "FNEW" $ \xftp -> X.createXFTPChunk xftp replicaKey fileInfo (L.map fst rKeys) auth + (sndId, rIds) <- withClient c tSess $ \xftp -> X.createXFTPChunk xftp replicaKey fileInfo (L.map fst rKeys) auth logServer "<--" c srv "" $ B.unwords ["SIDS", logSecret sndId] pure NewSndChunkReplica {server = srv, replicaId = ChunkReplicaId sndId, replicaKey, rcvIdsKeys = L.toList $ xftpRcvIdsKeys rIds rKeys} @@ -1887,33 +1848,6 @@ storeError = \case SEDatabaseBusy e -> CRITICAL True $ B.unpack e e -> INTERNAL $ show e -incStat :: AgentClient -> Int -> AgentStatsKey -> STM () -incStat AgentClient {agentStats} n k = do - TM.lookup k agentStats >>= \case - Just v -> modifyTVar' v (+ n) - _ -> newTVar n >>= \v -> TM.insert k v agentStats - -incClientStat :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> ByteString -> ByteString -> IO () -incClientStat c userId = incClientStat' c userId . protocolClient -{-# INLINE incClientStat #-} - -incClientStat' :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> ByteString -> ByteString -> IO () -incClientStat' c userId pc = incClientStatN c userId pc 1 -{-# INLINE incClientStat' #-} - -incServerStat :: AgentClient -> UserId -> ProtocolServer p -> ByteString -> ByteString -> IO () -incServerStat c userId ProtocolServer {host} cmd res = do - threadDelay 100000 - atomically $ incStat c 1 statsKey - where - statsKey = AgentStatsKey {userId, host = strEncode $ L.head host, clientTs = "", cmd, res} - -incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> Int -> ByteString -> ByteString -> IO () -incClientStatN c userId pc n cmd res = do - atomically $ incStat c n statsKey - where - statsKey = AgentStatsKey {userId, host = strEncode $ clientTransportHost pc, clientTs = strEncode $ clientSessionTs pc, cmd, res} - userServers :: forall p. (ProtocolTypeI p, UserProtocol p) => AgentClient -> TMap UserId (NonEmpty (ProtoServerWithAuth p)) userServers c = case protocolTypeI @p of SPSMP -> smpServers c diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index d52c12877..7c7a6cc17 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -957,6 +957,7 @@ testIncreaseConnAgentVersion t = do -- version doesn't increase if incompatible disposeAgentClient alice + threadDelay 250000 alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB runRight_ $ do @@ -968,6 +969,7 @@ testIncreaseConnAgentVersion t = do -- version increases if compatible disposeAgentClient bob + threadDelay 250000 bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 runRight_ $ do @@ -979,6 +981,7 @@ testIncreaseConnAgentVersion t = do -- version doesn't decrease, even if incompatible disposeAgentClient alice2 + threadDelay 250000 alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = mkVersionRange 2 2} initAgentServers testDB runRight_ $ do @@ -988,6 +991,7 @@ testIncreaseConnAgentVersion t = do checkVersion bob2 aliceId 3 disposeAgentClient bob2 + threadDelay 250000 bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = mkVersionRange 1 1} initAgentServers testDB2 runRight_ $ do @@ -1018,8 +1022,10 @@ testIncreaseConnAgentVersionMaxCompatible t = do -- version increases to max compatible disposeAgentClient alice + threadDelay 250000 alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB disposeAgentClient bob + threadDelay 250000 bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB2 runRight_ $ do @@ -1098,6 +1104,7 @@ testDuplicateMessage t = do get alice ##> ("", bobId, SENT 4) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False disposeAgentClient bob + threadDelay 250000 -- if the agent user did not send ACK, the message will be delivered again bob1 <- getSMPAgentClient' 3 agentCfg initAgentServers testDB2 @@ -1120,6 +1127,7 @@ testDuplicateMessage t = do disposeAgentClient alice disposeAgentClient bob1 + threadDelay 250000 alice2 <- getSMPAgentClient' 4 agentCfg initAgentServers testDB bob2 <- getSMPAgentClient' 5 agentCfg initAgentServers testDB2 @@ -1393,6 +1401,7 @@ setupDesynchronizedRatchet alice bob = do ackMessage alice bobId 7 Nothing disposeAgentClient bob + threadDelay 250000 -- importing database backup after progressing ratchet de-synchronizes ratchet liftIO $ renameFile (testDB2 <> ".bak") testDB2 @@ -3018,7 +3027,7 @@ noNetworkDelay a = do networkDelay :: AgentClient -> Int64 -> IO () networkDelay a d' = do d <- waitNetwork a - unless (d' < d && d < d' + 15000) $ expectationFailure $ "expected delay " <> show d' <> ", d = " <> show d + unless (d' - 1000 < d && d < d' + 15000) $ expectationFailure $ "expected delay " <> show d' <> ", d = " <> show d waitNetwork :: AgentClient -> IO Int64 waitNetwork a = do From a99ce6122ca0bab9935f54c3c1f1c80d5e11bb80 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Jun 2024 16:20:54 +0100 Subject: [PATCH 105/125] secure queue by sender via proxy (proxy SKEY command) (#1210) * client: secure queue by sender via proxy (proxy SKEY command) * agent and server: proxy SKEY command --- src/Simplex/Messaging/Agent/Client.hs | 42 ++++++++++++++++++--------- src/Simplex/Messaging/Client.hs | 28 +++++++++++------- src/Simplex/Messaging/Protocol.hs | 6 ++-- src/Simplex/Messaging/Server.hs | 27 ++++++++--------- 4 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index d049cfeb0..01d97f9ac 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1026,7 +1026,27 @@ withSMPClient c q cmdStr action = do withLogClient c tSess (queueId q) cmdStr $ action . connectedClient sendOrProxySMPMessage :: AgentClient -> UserId -> SMPServer -> ByteString -> Maybe SMP.SndPrivateAuthKey -> SMP.SenderId -> MsgFlags -> SMP.MsgBody -> AM (Maybe SMPServer) -sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do +sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = + sendOrProxySMPCommand c userId destSrv cmdStr senderId sendViaProxy sendDirectly + where + sendViaProxy smp proxySess = do + atomically $ incSMPServerStat c userId destSrv sentViaProxyAttempts + atomically $ incSMPServerStat c userId (protocolClientServer' smp) sentProxiedAttempts + proxySMPMessage smp proxySess spKey_ senderId msgFlags msg + sendDirectly smp = do + atomically $ incSMPServerStat c userId destSrv sentDirectAttempts + sendSMPMessage smp spKey_ senderId msgFlags msg + +sendOrProxySMPCommand :: + AgentClient -> + UserId -> + SMPServer -> + ByteString -> + SMP.SenderId -> + (SMPClient -> ProxiedRelay -> ExceptT SMPClientError IO (Either ProxyClientError ())) -> + (SMPClient -> ExceptT SMPClientError IO ()) -> + AM (Maybe SMPServer) +sendOrProxySMPCommand c userId destSrv cmdStr senderId sendCmdViaProxy sendCmdDirectly = do sess <- liftIO $ mkTransportSession c userId destSrv senderId ifM (atomically shouldUseProxy) (sendViaProxy sess) (sendDirectly sess $> Nothing) where @@ -1048,10 +1068,7 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) sendViaProxy destSess@(_, _, qId) = do r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do - r' <- liftClient SMP (clientServer smp) $ do - atomically $ incSMPServerStat c userId destSrv sentViaProxyAttempts - atomically $ incSMPServerStat c userId (protocolClientServer' smp) sentProxiedAttempts - proxySMPMessage smp proxySess spKey_ senderId msgFlags msg + r' <- liftClient SMP (clientServer smp) $ sendCmdViaProxy smp proxySess case r' of Right () -> pure . Just $ protocolClientServer' smp Left proxyErr -> do @@ -1089,11 +1106,7 @@ sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do | otherwise -> throwE e sendDirectly tSess = withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> do - r <- - tryAgentError $ - liftClient SMP (clientServer smp) $ do - atomically $ incSMPServerStat c userId destSrv sentDirectAttempts - sendSMPMessage smp spKey_ senderId msgFlags msg + r <- tryAgentError $ liftClient SMP (clientServer smp) $ sendCmdDirectly smp case r of Right () -> atomically $ incSMPServerStat c userId destSrv sentDirect Left e -> throwE e @@ -1536,9 +1549,12 @@ secureQueue c rq@RcvQueue {rcvId, rcvPrivateKey} senderKey = secureSMPQueue smp rcvPrivateKey rcvId senderKey secureSndQueue :: AgentClient -> SndQueue -> AM () -secureSndQueue c sq@SndQueue {sndId, sndPrivateKey, sndPublicKey} = - withSMPClient c sq "SKEY " $ \smp -> - secureSndSMPQueue smp sndPrivateKey sndId sndPublicKey +secureSndQueue c SndQueue {userId, server, sndId, sndPrivateKey, sndPublicKey} = + void $ sendOrProxySMPCommand c userId server "SKEY " sndId secureViaProxy secureDirectly + where + -- TODO track statistics + secureViaProxy smp proxySess = proxySecureSndSMPQueue smp proxySess sndPrivateKey sndId sndPublicKey + secureDirectly smp = secureSndSMPQueue smp sndPrivateKey sndId sndPublicKey enableQueueNotifications :: AgentClient -> RcvQueue -> SMP.NtfPublicAuthKey -> SMP.RcvNtfPublicDhKey -> AM (SMP.NotifierId, SMP.RcvNtfPublicDhKey) enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey = diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 39cf32677..e20b00039 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -48,6 +48,7 @@ module Simplex.Messaging.Client subscribeSMPQueuesNtfs, secureSMPQueue, secureSndSMPQueue, + proxySecureSndSMPQueue, enableSMPQueueNotifications, disableSMPQueueNotifications, enableSMPQueuesNtfs, @@ -59,7 +60,7 @@ module Simplex.Messaging.Client deleteSMPQueues, connectSMPProxiedRelay, proxySMPMessage, - forwardSMPMessage, + forwardSMPTransmission, getSMPQueueInfo, sendProtocolCommand, @@ -736,6 +737,10 @@ secureSndSMPQueue :: SMPClient -> SndPrivateAuthKey -> SenderId -> SndPublicAuth secureSndSMPQueue c spKey sId senderKey = okSMPCommand (SKEY senderKey) c spKey sId {-# INLINE secureSndSMPQueue #-} +proxySecureSndSMPQueue :: SMPClient -> ProxiedRelay -> SndPrivateAuthKey -> SenderId -> SndPublicAuthKey -> ExceptT SMPClientError IO (Either ProxyClientError ()) +proxySecureSndSMPQueue c proxiedRelay spKey sId senderKey = proxySMPCommand c proxiedRelay (Just spKey) sId (SKEY senderKey) +{-# INLINE proxySecureSndSMPQueue #-} + -- | Enable notifications for the queue for push notifications server. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#enable-notifications-command @@ -776,6 +781,9 @@ sendSMPMessage c spKey sId flags msg = OK -> pure () r -> throwE $ unexpectedResponse r +proxySMPMessage :: SMPClient -> ProxiedRelay -> Maybe SndPrivateAuthKey -> SenderId -> MsgFlags -> MsgBody -> ExceptT SMPClientError IO (Either ProxyClientError ()) +proxySMPMessage c proxiedRelay spKey sId flags msg = proxySMPCommand c proxiedRelay spKey sId (SEND flags msg) + -- | Acknowledge message delivery (server deletes the message). -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery @@ -877,24 +885,24 @@ instance StrEncoding ProxyClientError where -- 8) PFWD(SEND) -> WTF -> ProxyUnexpectedResponse - client/proxy protocol logic -- 9) PFWD(SEND) -> ??? -> ProxyResponseError - client/proxy syntax -- --- We report as proxySMPMessage error (ExceptT error) the errors of two kinds: +-- We report as proxySMPCommand error (ExceptT error) the errors of two kinds: -- - protocol errors from the destination relay wrapped in PRES - to simplify processing of AUTH and QUOTA errors, in this case proxy is "transparent" for such errors (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) -- - other response/transport/connection errors from the client connected to proxy itself -- Other errors are reported in the function result as `Either ProxiedRelayError ()`, including -- - protocol errors from the client connected to proxy in ProxyClientError (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) -- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError -proxySMPMessage :: +-- This function proxies Sender commands that return OK or ERR +proxySMPCommand :: SMPClient -> -- proxy session from PKEY ProxiedRelay -> -- message to deliver Maybe SndPrivateAuthKey -> SenderId -> - MsgFlags -> - MsgBody -> + Command 'Sender -> ExceptT SMPClientError IO (Either ProxyClientError ()) -proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do +proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId command = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} @@ -902,14 +910,14 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c let cmdSecret = C.dh' serverKey cmdPrivKey nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g -- encode - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender (SEND flags msg)) + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender command) auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of [] -> throwE $ PCETransportError TELargeMsg TBError e _ : _ -> throwE $ PCETransportError e TBTransmission s _ : _ -> pure s TBTransmissions s _ _ : _ -> pure s - et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength + et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedTLength -- proxy interaction errors are wrapped let tOut = Just $ 2 * tcpTimeout tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case @@ -937,8 +945,8 @@ proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c -- sends RFWD :: EncFwdTransmission -> Command Sender -- receives RRES :: EncFwdResponse -> BrokerMsg -- proxy should send PRES to the client with EncResponse -forwardSMPMessage :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse -forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do +forwardSMPTransmission :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of Nothing -> throwE $ PCETransportError TENoServerAuth diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index d1b3f85d6..63e3e4d98 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -43,7 +43,7 @@ module Simplex.Messaging.Protocol ( -- * SMP protocol parameters supportedSMPClientVRange, maxMessageLength, - paddedProxiedMsgLength, + paddedProxiedTLength, e2eEncConfirmationLength, e2eEncMessageLength, @@ -258,8 +258,8 @@ maxMessageLength v | v >= sendingProxySMPVersion = 16064 -- max 16067 | otherwise = 16088 -- 16064 - always use this size to determine allowed ranges -paddedProxiedMsgLength :: Int -paddedProxiedMsgLength = 16242 -- 16241 .. 16243 +paddedProxiedTLength :: Int +paddedProxiedTLength = 16242 -- 16241 .. 16243 -- TODO v6.0 change to 16064 type MaxMessageLen = 16088 diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index f673018bf..e96e8b582 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -70,7 +70,7 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, forwardSMPMessage, smpProxyError, temporaryClientError) +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, forwardSMPTransmission, smpProxyError, temporaryClientError) import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient, getConnectedSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -742,7 +742,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi inc own pRequests if v >= sendingProxySMPVersion then forkProxiedCmd $ do - liftIO (runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case + liftIO (runExceptT (forwardSMPTransmission smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case Right r -> PRES r <$ inc own pSuccesses Left e -> ERR (smpProxyError e) <$ case e of PCEProtocolError {} -> inc own pSuccesses @@ -1095,16 +1095,13 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi t :| [] -> pure $ tDecodeParseValidate clntTHParams t _ -> throwE BLOCK let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} - -- process forwarded SEND + -- process forwarded command r <- lift (rejectOrVerify clntThAuth t') >>= \case Left r -> pure r - Right t''@(_, (corrId', entId', cmd')) -> case cmd' of - Cmd SSender SEND {} -> - -- Left will not be returned by processCommand, as only SEND command is allowed - fromMaybe (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') - _ -> - pure (corrId', entId', ERR $ CMD PROHIBITED) + -- rejectOrVerify filters allowed commands, no need to repeat it here. + -- INTERNAL is used because processCommand never returns Nothing for sender commands (could be extracted for better types). + Right t''@(_, (corrId', entId', _)) -> fromMaybe (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') -- encode response r' <- case batchTransmissions (batch clntTHParams) (blockSize clntTHParams) [Right (Nothing, encodeTransmission clntTHParams r)] of [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right @@ -1112,7 +1109,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi TBTransmission b' _ : _ -> pure b' TBTransmissions b' _ _ : _ -> pure b' -- encrypt to client - r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedTLength -- encrypt to proxy let fr = FwdResponse {fwdCorrId, fwdResponse = r2} r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) @@ -1124,13 +1121,17 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = case cmdOrError of Left e -> pure $ Left (corrId', entId', ERR e) - Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' + Right cmd' + | allowed -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' + | otherwise -> pure $ Left (corrId', entId', ERR $ CMD PROHIBITED) where + allowed = case cmd' of + Cmd SSender SEND {} -> True + Cmd SSender (SKEY _) -> True + _ -> False verified = \case VRVerified qr -> Right (qr, (corrId', entId', cmd')) VRFailed -> Left (corrId', entId', ERR AUTH) - Right _ -> pure $ Left (corrId', entId', ERR $ CMD PROHIBITED) - deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> M (Transmission BrokerMsg) deliverMessage name qr rId sub q msg_ = time (name <> " deliver") $ do readTVarIO sub >>= \case From 26cfad5e88772a58e10efa43ac58bfb584977497 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 1 Jul 2024 13:42:46 +0100 Subject: [PATCH 106/125] do not use sndSecure when rotating queue (#1213) --- src/Simplex/Messaging/Agent.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index c6942af8f..63f59a9c5 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1565,14 +1565,13 @@ switchConnection' c connId = _ -> throwE $ CMD PROHIBITED "switchConnection: not duplex" switchDuplexConnection :: AgentClient -> Connection 'CDuplex -> RcvQueue -> AM ConnectionStats -switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId, connAgentVersion} rqs sqs) rq@RcvQueue {server, dbQueueId = DBQueueId dbQueueId, sndId} = do +switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs sqs) rq@RcvQueue {server, dbQueueId = DBQueueId dbQueueId, sndId} = do checkRQSwchStatus rq RSSwitchStarted clientVRange <- asks $ smpClientVRange . config -- try to get the server that is different from all queues, or at least from the primary rcv queue srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs) srv' <- if srv == server then getNextServer c userId [server] else pure srvAuth - let sndSecure = connAgentVersion >= sndAuthKeySMPAgentVersion - (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe sndSecure + (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe False let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq' lift $ addNewQueueSubscription c rq'' tSess sessId From a50e2e74a587fa5363251a4614a1f3c0730dc390 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 2 Jul 2024 00:06:38 +0400 Subject: [PATCH 107/125] agent: disable saving stats (#1214) * agent: disable stat saving * disable migration * schema * disable restore --- src/Simplex/Messaging/Agent.hs | 21 ++++++++++--------- .../Agent/Store/SQLite/Migrations.hs | 4 ++-- .../Store/SQLite/Migrations/agent_schema.sql | 7 ------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index c4f62702e..72b7bcb7b 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -210,14 +210,14 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = runAgentThreads c | backgroundMode = run c "subscriber" $ subscriber c | otherwise = do - restoreServersStats c + -- restoreServersStats c raceAny_ [ run c "subscriber" $ subscriber c, run c "runNtfSupervisor" $ runNtfSupervisor c, - run c "cleanupManager" $ cleanupManager c, - run c "logServersStats" $ logServersStats c + run c "cleanupManager" $ cleanupManager c + -- run c "logServersStats" $ logServersStats c ] - `E.finally` saveServersStats c + -- `E.finally` saveServersStats c run AgentClient {subQ, acThread} name a = a `E.catchAny` \e -> whenM (isJust <$> readTVarIO acThread) $ do logError $ "Agent thread " <> name <> " crashed: " <> tshow e @@ -234,12 +234,13 @@ logServersStats c = do saveServersStats :: AgentClient -> AM' () saveServersStats c@AgentClient {subQ, smpServersStats, xftpServersStats} = do - sss <- mapM (lift . getAgentSMPServerStats) =<< readTVarIO smpServersStats - xss <- mapM (lift . getAgentXFTPServerStats) =<< readTVarIO xftpServersStats - let stats = AgentPersistedServerStats {smpServersStats = sss, xftpServersStats = xss} - tryAgentError' (withStore' c (`updateServersStats` stats)) >>= \case - Left e -> atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL $ show e) - Right () -> pure () + -- sss <- mapM (lift . getAgentSMPServerStats) =<< readTVarIO smpServersStats + -- xss <- mapM (lift . getAgentXFTPServerStats) =<< readTVarIO xftpServersStats + -- let stats = AgentPersistedServerStats {smpServersStats = sss, xftpServersStats = xss} + -- tryAgentError' (withStore' c (`updateServersStats` stats)) >>= \case + -- Left e -> atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL $ show e) + -- Right () -> pure () + pure () restoreServersStats :: AgentClient -> AM' () restoreServersStats c@AgentClient {smpServersStats, xftpServersStats, srvStatsStartedAt} = do diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index 340063f5c..9dd10d1b6 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -113,8 +113,8 @@ schemaMigrations = ("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect), ("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery), ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem), - ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays), - ("m20240518_servers_stats", m20240518_servers_stats, Just down_m20240518_servers_stats) + ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays) + -- ("m20240518_servers_stats", m20240518_servers_stats, Just down_m20240518_servers_stats) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index 50cf6d74a..caf94418a 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -394,13 +394,6 @@ CREATE TABLE processed_ratchet_key_hashes( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); -CREATE TABLE servers_stats( - servers_stats_id INTEGER PRIMARY KEY, - servers_stats TEXT, - started_at TEXT NOT NULL DEFAULT(datetime('now')), - created_at TEXT NOT NULL DEFAULT(datetime('now')), - updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id); CREATE UNIQUE INDEX idx_rcv_queue_id ON rcv_queues(conn_id, rcv_queue_id); CREATE UNIQUE INDEX idx_snd_queue_id ON snd_queues(conn_id, snd_queue_id); From da5f6691336e90feffd6c0043b70a2f477d1cc7f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 1 Jul 2024 23:34:31 +0100 Subject: [PATCH 108/125] remove diagram --- .../duplex-messaging/duplex-creating-v6.mmd | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd diff --git a/protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd b/protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd deleted file mode 100644 index 183e4bfa9..000000000 --- a/protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd +++ /dev/null @@ -1,63 +0,0 @@ -sequenceDiagram - participant A as Alice - participant AA as Alice's
agent - participant AS as Alice's
server - participant BS as Bob's
server - participant BA as Bob's
agent - participant B as Bob - - note over AA, BA: status (receive/send): NONE/NONE - - note over A, AA: 1. request connection
from agent - A ->> AA: NEW: create
duplex connection - - note over AA, AS: 2. create Alice's SMP queue - AA ->> AS: NEW: create SMP queue - AS ->> AA: IDS: SMP queue IDs - note over AA: status: NEW/NONE - - AA ->> A: INV: invitation
to connect - - note over A, B: 3. out-of-band invitation - A ->> B: OOB: invitation to connect - - note over BA, B: 4. accept connection - B ->> BA: JOIN:
via invitation info - note over BA: status: NONE/NEW - - note over BA, AS: 5. secure Alice's SMP queue - BA ->> AS: SKEY: secure queue (this command needs to be proxied) - note over BA: status: NONE/SECURED - - note over BA, BS: 6. create Bob's SMP queue - BA ->> BS: NEW: create SMP queue - BS ->> BA: IDS: SMP queue IDs - note over BA: status: NEW/SECURED - - note over BA, AA: 7. confirm Alice's SMP queue - BA ->> AS: SEND: Bob's info without sender's key (SMP confirmation with reply queues) - note over BA: status: NEW/CONFIRMED - - AS ->> AA: MSG: Bob's info without
sender server key - note over AA: status: CONFIRMED/NEW - AA ->> AS: ACK: confirm message - - note over AA, BS: 8. secure Bob's SMP queue - AA ->> BS: SKEY: secure queue (this command needs to be proxied) - note over BA: status: CONFIRMED/SECURED - - AA ->> BS: SEND: Alice's info without sender's server key (SMP confirmation without reply queues) - note over AA: status: CONFIRMED/CONFIRMED - - note over AA, A: 9. notify Alice
about connection success
(no HELLO needed in v6) - AA ->> A: CON: connected - note over AA: status: ACTIVE/ACTIVE - - BS ->> BA: MSG: Alice's info without
sender's server key - note over BA: status: CONFIRMED/CONFIRMED - BA ->> B: INFO: Alice's info - BA ->> BS: ACK: confirm message - - note over BA, B: 10. notify Bob
about connection success - BA ->> B: CON: connected - note over BA: status: ACTIVE/ACTIVE From aa1d8d6c8bcff82f02aa580b1b729fc4b1396fd9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 2 Jul 2024 13:40:37 +0100 Subject: [PATCH 109/125] agent: disable fast handshake (#1215) --- src/Simplex/Messaging/Agent.hs | 4 +- tests/AgentTests/FunctionalAPITests.hs | 110 ++++++++++++------------- tests/AgentTests/NotificationTests.hs | 14 ++-- tests/SMPProxyTests.hs | 18 ++-- 4 files changed, 73 insertions(+), 73 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 29ea05ead..32bfa4198 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -739,7 +739,7 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - let sndSecure = case cMode of SCMInvitation -> True; SCMContact -> False + let sndSecure = False -- case cMode of SCMInvitation -> True; SCMContact -> False (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srvWithAuth smpClientVRange subMode sndSecure `catchAgentError` \e -> liftIO (print e) >> throwE e atomically $ incSMPServerStat c userId srv connCreated rq' <- withStore c $ \db -> updateNewConnRcv db connId rq @@ -883,7 +883,7 @@ joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do - let sndSecure = smpClientVersion >= sndAuthKeySMPClientVersion + let sndSecure = False -- smpClientVersion >= sndAuthKeySMPClientVersion (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode sndSecure let qInfo = toVersionT qUri smpClientVersion rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index b06cc9194..19f4977fc 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -257,11 +257,11 @@ functionalAPITests :: ATransport -> Spec functionalAPITests t = do describe "Establishing duplex connection" $ do testMatrix2 t runAgentClientTest - it "should connect when server with multiple identities is stored" $ + xit "should connect when server with multiple identities is stored" $ withSmpServer t testServerMultipleIdentities - it "should connect with two peers" $ + xit "should connect with two peers" $ withSmpServer t testAgentClient3 - it "should establish connection without PQ encryption and enable it" $ + xit "should establish connection without PQ encryption and enable it" $ withSmpServer t testEnablePQEncryption describe "Establishing duplex connection, different PQ settings" $ do testPQMatrix2 t $ runAgentClientTestPQ True @@ -290,43 +290,43 @@ functionalAPITests t = do testAllowConnectionClientRestart t describe "Message delivery" $ do describe "update connection agent version on received messages" $ do - it "should increase if compatible, shouldn't decrease" $ + xit "should increase if compatible, shouldn't decrease" $ testIncreaseConnAgentVersion t - it "should increase to max compatible version" $ + xit "should increase to max compatible version" $ testIncreaseConnAgentVersionMaxCompatible t - it "should increase when connection was negotiated on different versions" $ + xit "should increase when connection was negotiated on different versions" $ testIncreaseConnAgentVersionStartDifferentVersion t -- TODO PQ tests for upgrading connection to PQ encryption - it "should deliver message after client restart" $ + xit "should deliver message after client restart" $ testDeliverClientRestart t - it "should deliver messages to the user once, even if repeat delivery is made by the server (no ACK)" $ + xit "should deliver messages to the user once, even if repeat delivery is made by the server (no ACK)" $ testDuplicateMessage t - it "should report error via msg integrity on skipped messages" $ + xit "should report error via msg integrity on skipped messages" $ testSkippedMessages t - it "should connect to the server when server goes up if it initially was down" $ + xit "should connect to the server when server goes up if it initially was down" $ testDeliveryAfterSubscriptionError t - it "should deliver messages if one of connections has quota exceeded" $ + xit "should deliver messages if one of connections has quota exceeded" $ testMsgDeliveryQuotaExceeded t describe "message expiration" $ do - it "should expire one message" $ testExpireMessage t - it "should expire multiple messages" $ testExpireManyMessages t - it "should expire one message if quota is exceeded" $ testExpireMessageQuota t - it "should expire multiple messages if quota is exceeded" $ testExpireManyMessagesQuota t + xit "should expire one message" $ testExpireMessage t + xit "should expire multiple messages" $ testExpireManyMessages t + xit "should expire one message if quota is exceeded" $ testExpireMessageQuota t + xit "should expire multiple messages if quota is exceeded" $ testExpireManyMessagesQuota t describe "Ratchet synchronization" $ do - it "should report ratchet de-synchronization, synchronize ratchets" $ + xit "should report ratchet de-synchronization, synchronize ratchets" $ testRatchetSync t - it "should synchronize ratchets after server being offline" $ + xit "should synchronize ratchets after server being offline" $ testRatchetSyncServerOffline t - it "should synchronize ratchets after client restart" $ + xit "should synchronize ratchets after client restart" $ testRatchetSyncClientRestart t - it "should synchronize ratchets after suspend/foreground" $ + xit "should synchronize ratchets after suspend/foreground" $ testRatchetSyncSuspendForeground t - it "should synchronize ratchets when clients start synchronization simultaneously" $ + xit "should synchronize ratchets when clients start synchronization simultaneously" $ testRatchetSyncSimultaneous t describe "Subscription mode OnlyCreate" $ do it "messages delivered only when polled (v8 - slow handshake)" $ withSmpServer t testOnlyCreatePullSlowHandshake - it "messages delivered only when polled" $ + xit "messages delivered only when polled" $ withSmpServer t testOnlyCreatePull describe "Inactive client disconnection" $ do it "should disconnect clients without subs if they were inactive longer than TTL" $ @@ -336,14 +336,14 @@ functionalAPITests t = do it "should NOT disconnect active clients" $ testActiveClientNotDisconnected t describe "Suspending agent" $ do - it "should update client when agent is suspended" $ + xit "should update client when agent is suspended" $ withSmpServer t testSuspendingAgent - it "should complete sending messages when agent is suspended" $ + xit "should complete sending messages when agent is suspended" $ testSuspendingAgentCompleteSending t - it "should suspend agent on timeout, even if pending messages not sent" $ + xit "should suspend agent on timeout, even if pending messages not sent" $ testSuspendingAgentTimeout t describe "Batching SMP commands" $ do - it "should subscribe to multiple (200) subscriptions with batching" $ + xit "should subscribe to multiple (200) subscriptions with batching" $ testBatchedSubscriptions 200 10 t skip "faster version of the previous test (200 subscriptions gets very slow with test coverage)" $ it "should subscribe to multiple (6) subscriptions with batching" $ @@ -362,7 +362,7 @@ functionalAPITests t = do testDeleteConnectionAsync t it "join connection when reply queue creation fails (v8 - slow handshake)" $ testJoinConnectionAsyncReplyErrorV8 t - it "join connection when reply queue creation fails" $ + xit "join connection when reply queue creation fails" $ testJoinConnectionAsyncReplyError t describe "delete connection waiting for delivery" $ do it "should delete connection immediately if there are no pending messages" $ @@ -376,34 +376,34 @@ functionalAPITests t = do it "should delete connection by timeout, message in progress can be delivered" $ testWaitDeliveryTimeout2 t describe "Users" $ do - it "should create and delete user with connections" $ + xit "should create and delete user with connections" $ withSmpServer t testUsers - it "should create and delete user without connections" $ + xit "should create and delete user without connections" $ withSmpServer t testDeleteUserQuietly - it "should create and delete user with connections when server connection fails" $ + xit "should create and delete user with connections when server connection fails" $ testUsersNoServer t - it "should connect two users and switch session mode" $ + xit "should connect two users and switch session mode" $ withSmpServer t testTwoUsers describe "Connection switch" $ do - describe "should switch delivery to the new queue" $ + xdescribe "should switch delivery to the new queue" $ testServerMatrix2 t testSwitchConnection - describe "should switch to new queue asynchronously" $ + xdescribe "should switch to new queue asynchronously" $ testServerMatrix2 t testSwitchAsync describe "should delete connection during switch" $ testServerMatrix2 t testSwitchDelete - describe "should abort switch in Started phase" $ + xdescribe "should abort switch in Started phase" $ testServerMatrix2 t testAbortSwitchStarted - describe "should abort switch in Started phase, reinitiate immediately" $ + xdescribe "should abort switch in Started phase, reinitiate immediately" $ testServerMatrix2 t testAbortSwitchStartedReinitiate - describe "should prohibit to abort switch in Secured phase" $ + xdescribe "should prohibit to abort switch in Secured phase" $ testServerMatrix2 t testCannotAbortSwitchSecured - describe "should switch two connections simultaneously" $ + xdescribe "should switch two connections simultaneously" $ testServerMatrix2 t testSwitch2Connections - describe "should switch two connections simultaneously, abort one" $ + xdescribe "should switch two connections simultaneously, abort one" $ testServerMatrix2 t testSwitch2ConnectionsAbort1 describe "SMP basic auth" $ do let v4 = prevVersion basicAuthSMPVersion - forM_ (nub [prevVersion authCmdsSMPVersion, authCmdsSMPVersion, currentServerSMPRelayVersion]) $ \v -> do + forM_ (nub [prevVersion authCmdsSMPVersion, authCmdsSMPVersion, prevVersion currentServerSMPRelayVersion]) $ \v -> do let baseId = if v >= sndAuthKeySMPVersion then 1 else 3 describe ("v" <> show v <> ": with server auth") $ do -- allow NEW | server auth, v | clnt1 auth, v | clnt2 auth, v | 2 - success, 1 - JOIN fail, 0 - NEW fail @@ -444,15 +444,15 @@ functionalAPITests t = do it "should return the same data for both peers" $ withSmpServer t testRatchetAdHash describe "Delivery receipts" $ do - it "should send and receive delivery receipt" $ withSmpServer t testDeliveryReceipts - it "should send delivery receipt only in connection v3+" $ testDeliveryReceiptsVersion t + xit "should send and receive delivery receipt" $ withSmpServer t testDeliveryReceipts + xit "should send delivery receipt only in connection v3+" $ testDeliveryReceiptsVersion t it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent t describe "user network info" $ do it "should wait for user network" testWaitForUserNetwork it "should not reset online to offline if happens too quickly" testDoNotResetOnlineToOffline it "should resume multiple threads" testResumeMultipleThreads describe "SMP queue info" $ do - it "server should respond with queue and subscription information" $ + xit "server should respond with queue and subscription information" $ withSmpServer t testServerQueueInfo testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> AgentMsgId -> IO Int @@ -475,28 +475,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = testMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + xit "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False + xit "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False testBasicMatrix2 :: HasCallStack => ATransport -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testBasicMatrix2 t runTest = do - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest + xit "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrevPQ agentCfgVPrevPQ 3 $ runTest it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrevPQ agentCfg 3 $ runTest it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrevPQ 3 $ runTest testRatchetMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + xit "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True - it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False - it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 1 $ runTest PQSupportOff False - it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 1 $ runTest PQSupportOff False - it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 1 $ runTest PQSupportOff False + xit "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False + xit "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 1 $ runTest PQSupportOff False + xit "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 1 $ runTest PQSupportOff False + xit "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 1 $ runTest PQSupportOff False testServerMatrix2 :: HasCallStack => ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -1947,7 +1947,7 @@ testWaitDeliveryNoPending t = withAgentClients2 $ \alice bob -> liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 1 + baseId = 3 msgId = subtract baseId testWaitDelivery :: ATransport -> IO () @@ -2001,7 +2001,7 @@ testWaitDelivery t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 1 + baseId = 3 msgId = subtract baseId testWaitDeliveryAUTHErr :: ATransport -> IO () @@ -2044,7 +2044,7 @@ testWaitDeliveryAUTHErr t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 1 + baseId = 3 msgId = subtract baseId testWaitDeliveryTimeout :: ATransport -> IO () @@ -2084,7 +2084,7 @@ testWaitDeliveryTimeout t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 1 + baseId = 3 msgId = subtract baseId testWaitDeliveryTimeout2 :: ATransport -> IO () @@ -2130,7 +2130,7 @@ testWaitDeliveryTimeout2 t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 1 + baseId = 3 msgId = subtract baseId testJoinConnectionAsyncReplyErrorV8 :: HasCallStack => ATransport -> IO () @@ -3087,7 +3087,7 @@ exchangeGreetings :: HasCallStack => AgentClient -> ConnId -> AgentClient -> Con exchangeGreetings = exchangeGreetings_ PQEncOn exchangeGreetings_ :: HasCallStack => PQEncryption -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () -exchangeGreetings_ pqEnc = exchangeGreetingsMsgId_ pqEnc 2 +exchangeGreetings_ pqEnc = exchangeGreetingsMsgId_ pqEnc 4 exchangeGreetingsMsgId :: HasCallStack => Int64 -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetingsMsgId = exchangeGreetingsMsgId_ PQEncOn diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index ab9d8453c..603ffd3c0 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -105,7 +105,7 @@ notificationTests t = do describe "Managing notification subscriptions" $ do describe "should create notification subscription for existing connection" $ testNtfMatrix t testNotificationSubscriptionExistingConnection - describe "should create notification subscription for new connection" $ + xdescribe "should create notification subscription for new connection" $ testNtfMatrix t testNotificationSubscriptionNewConnection it "should change notifications mode" $ withSmpServer t $ @@ -116,19 +116,19 @@ notificationTests t = do withAPNSMockServer $ \apns -> withNtfServer t $ testChangeToken apns describe "Notifications server store log" $ - it "should save and restore tokens and subscriptions" $ + xit "should save and restore tokens and subscriptions" $ withSmpServer t $ withAPNSMockServer $ \apns -> testNotificationsStoreLog t apns describe "Notifications after SMP server restart" $ - it "should resume subscriptions after SMP server is restarted" $ + xit "should resume subscriptions after SMP server is restarted" $ withAPNSMockServer $ \apns -> withNtfServer t $ testNotificationsSMPRestart t apns describe "Notifications after SMP server restart" $ it "should resume batched subscriptions after SMP server is restarted" $ withAPNSMockServer $ \apns -> withNtfServer t $ testNotificationsSMPRestartBatch 100 t apns - describe "should switch notifications to the new queue" $ + xdescribe "should switch notifications to the new queue" $ testServerMatrix2 t $ \servers -> withAPNSMockServer $ \apns -> withNtfServer t $ testSwitchNotifications servers apns @@ -146,7 +146,7 @@ notificationTests t = do testNtfMatrix :: HasCallStack => ATransport -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> Spec testNtfMatrix t runTest = do describe "next and current" $ do - it "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest + xit "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest it "curr servers; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest it "prev servers; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest it "prev servers; curr clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest @@ -515,7 +515,7 @@ testChangeNotificationsMode APNSMockServer {apnsQ} = -- no notifications should follow noNotification apnsQ where - baseId = 1 + baseId = 3 msgId = subtract baseId testChangeToken :: APNSMockServer -> IO () @@ -554,7 +554,7 @@ testChangeToken APNSMockServer {apnsQ} = withAgent 1 agentCfg initAgentServers t -- no notifications should follow noNotification apnsQ where - baseId = 1 + baseId = 3 msgId = subtract baseId testNotificationsStoreLog :: ATransport -> APNSMockServer -> IO () diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 6452d2677..e05ff884d 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -101,26 +101,26 @@ smpProxyTests = do it "500x20" . twoServersFirstProxy $ 500 `inParrallel` deliver 20 describe "agent API" $ do describe "one server" $ do - it "always via proxy" . oneServer $ + xit "always via proxy" . oneServer $ agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1 - it "without proxy" . oneServer $ + xit "without proxy" . oneServer $ agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 describe "two servers" $ do - it "always via proxy" . twoServers $ + xit "always via proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1 - it "both via proxy" . twoServers $ + xit "both via proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" 1 - it "first via proxy" . twoServers $ + xit "first via proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 - it "without proxy" . twoServers $ + xit "without proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 - it "first via proxy for unknown" . twoServers $ + xit "first via proxy for unknown" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 1 it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 3 it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ agentViaProxyVersionError - it "retries sending when destination or proxy relay is offline" $ + xit "retries sending when destination or proxy relay is offline" $ agentViaProxyRetryOffline describe "stress test 1k" $ do let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) @@ -370,7 +370,7 @@ agentViaProxyRetryOffline = do a `up` cId = nGet a =##> \case ("", "", UP _ [c]) -> c == cId; _ -> False a `down` cId = nGet a =##> \case ("", "", DOWN _ [c]) -> c == cId; _ -> False aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval} - baseId = 1 + baseId = 3 msgId = subtract baseId . fst servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]} From f392ce0a9355cd3883400906ae6c361b77ca46ea Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 2 Jul 2024 13:55:28 +0100 Subject: [PATCH 110/125] 5.8.2.0 --- CHANGELOG.md | 10 ++++++++++ package.yaml | 2 +- simplexmq.cabal | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707a6ff5f..90706a19e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 5.8.2 + +Agent: +- fast handshake support (disabled). +- new statistics api. + +SMP server: +- fast handshake support (SKEY command). +- minor changes to reduce memory usage. + # 5.8.1 Agent: diff --git a/package.yaml b/package.yaml index 16e75c866..94c2b6db0 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.1.0 +version: 5.8.2.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index d46b58dc2..d1fa32d43 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.1.0 +version: 5.8.2.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From ae8e1c5e9aa3155907f1bd075e9c69af5fce2bee Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:36:15 +0400 Subject: [PATCH 111/125] agent: servers stats improvements, fixes (#1208) * agent: reset stats startedAt time in memory * getAgentSubsSummary * change sub counting * ack statistics * add import * instance * Revert "instance" This reverts commit 1f63740d565d43d033558ee62cfb5a8c2fe4575d. * Revert "add import" This reverts commit ef72df80144044a0c1eefa1bffab2fd01d0851b4. * modify sub counting * modify conn creation counting * use int64 * file size stats * remove import * ack err counting * conn del stats * format * new data * add data * toKB * restore connCompleted * use Int for counts * use rq from scope * remove getAgentSubsSummary * fix connCompleted * fix * revert disabling stats * use srv from scope * combine ack stats * modify * comment * count subs * refactor --------- Co-authored-by: Evgeny Poberezkin --- simplexmq.cabal | 2 +- src/Simplex/FileTransfer/Agent.hs | 8 +- src/Simplex/FileTransfer/Chunks.hs | 4 + src/Simplex/Messaging/Agent.hs | 76 +++++--- src/Simplex/Messaging/Agent/Client.hs | 48 +++-- src/Simplex/Messaging/Agent/Stats.hs | 182 +++++++++++++++++- src/Simplex/Messaging/Agent/Store.hs | 6 + src/Simplex/Messaging/Agent/Store/SQLite.hs | 7 +- .../Agent/Store/SQLite/Migrations.hs | 4 +- ...rs_stats.hs => M20240702_servers_stats.hs} | 10 +- .../Store/SQLite/Migrations/agent_schema.sql | 7 + 11 files changed, 295 insertions(+), 59 deletions(-) rename src/Simplex/Messaging/Agent/Store/SQLite/Migrations/{M20240518_servers_stats.hs => M20240702_servers_stats.hs} (79%) diff --git a/simplexmq.cabal b/simplexmq.cabal index d1fa32d43..aec362196 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -133,8 +133,8 @@ library Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays - Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure + Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats Simplex.Messaging.Agent.TRcvQueues Simplex.Messaging.Client Simplex.Messaging.Client.Agent diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index 90dda5cbc..4683143c5 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -49,6 +49,7 @@ import qualified Data.Set as S import Data.Text (Text) import Data.Time.Clock (getCurrentTime) import Data.Time.Format (defaultTimeLocale, formatTime) +import Simplex.FileTransfer.Chunks (toKB) import Simplex.FileTransfer.Client (XFTPChunkSpec (..)) import Simplex.FileTransfer.Client.Main import Simplex.FileTransfer.Crypto @@ -206,7 +207,8 @@ runXFTPRcvWorker c srv Worker {doWork} = do unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ FILE NOT_APPROVED fsFileTmpPath <- lift $ toFSFilePath fileTmpPath chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo - let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest) + let chSize = unFileSize chunkSize + chunkSpec = XFTPRcvChunkSpec chunkPath chSize (unFileDigest digest) relChunkPath = fileTmpPath takeFileName chunkPath agentXFTPDownloadChunk c userId digest replica chunkSpec atomically $ waitUntilForeground c @@ -221,6 +223,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived pure (entityId, complete, RFPROG rcvd total) atomically $ incXFTPServerStat c userId srv downloads + atomically $ incXFTPServerSizeStat c userId srv downloadsSize (fromIntegral $ toKB chSize) notify c entityId progress when complete . lift . void $ getXFTPRcvWorker True c Nothing @@ -506,7 +509,7 @@ runXFTPSndWorker c srv Worker {doWork} = do atomically $ incXFTPServerStat c userId srv uploadErrs sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) e uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> AM () - uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do + uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath, chunkSize = chSize}, digest = chunkDigest} replica = do replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica fsFilePath <- lift $ toFSFilePath filePath unlessM (doesFileExist fsFilePath) $ throwE $ FILE NO_FILE @@ -521,6 +524,7 @@ runXFTPSndWorker c srv Worker {doWork} = do total = totalSize chunks complete = all chunkUploaded chunks atomically $ incXFTPServerStat c userId srv uploads + atomically $ incXFTPServerSizeStat c userId srv uploadsSize (fromIntegral $ toKB chSize) notify c sndFileEntityId $ SFPROG uploaded total when complete $ do (sndDescr, rcvDescrs) <- sndFileToDescrs sf diff --git a/src/Simplex/FileTransfer/Chunks.hs b/src/Simplex/FileTransfer/Chunks.hs index 0b35649c5..d8890944d 100644 --- a/src/Simplex/FileTransfer/Chunks.hs +++ b/src/Simplex/FileTransfer/Chunks.hs @@ -26,6 +26,10 @@ kb :: Integral a => a -> a kb n = 1024 * n {-# INLINE kb #-} +toKB :: Integral a => a -> a +toKB n = n `div` 1024 +{-# INLINE toKB #-} + mb :: Integral a => a -> a mb n = 1024 * kb n {-# INLINE mb #-} diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 32bfa4198..480c8f801 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -157,6 +157,7 @@ import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Stats +import Simplex.Messaging.Agent.Stats (AgentSMPServerStats (connSubErrs)) import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -210,14 +211,14 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = runAgentThreads c | backgroundMode = run c "subscriber" $ subscriber c | otherwise = do - -- restoreServersStats c + restoreServersStats c raceAny_ [ run c "subscriber" $ subscriber c, run c "runNtfSupervisor" $ runNtfSupervisor c, - run c "cleanupManager" $ cleanupManager c - -- run c "logServersStats" $ logServersStats c + run c "cleanupManager" $ cleanupManager c, + run c "logServersStats" $ logServersStats c ] - -- `E.finally` saveServersStats c + `E.finally` saveServersStats c run AgentClient {subQ, acThread} name a = a `E.catchAny` \e -> whenM (isJust <$> readTVarIO acThread) $ do logError $ "Agent thread " <> name <> " crashed: " <> tshow e @@ -234,13 +235,12 @@ logServersStats c = do saveServersStats :: AgentClient -> AM' () saveServersStats c@AgentClient {subQ, smpServersStats, xftpServersStats} = do - -- sss <- mapM (lift . getAgentSMPServerStats) =<< readTVarIO smpServersStats - -- xss <- mapM (lift . getAgentXFTPServerStats) =<< readTVarIO xftpServersStats - -- let stats = AgentPersistedServerStats {smpServersStats = sss, xftpServersStats = xss} - -- tryAgentError' (withStore' c (`updateServersStats` stats)) >>= \case - -- Left e -> atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL $ show e) - -- Right () -> pure () - pure () + sss <- mapM (lift . getAgentSMPServerStats) =<< readTVarIO smpServersStats + xss <- mapM (lift . getAgentXFTPServerStats) =<< readTVarIO xftpServersStats + let stats = AgentPersistedServerStats {smpServersStats = sss, xftpServersStats = xss} + tryAgentError' (withStore' c (`updateServersStats` stats)) >>= \case + Left e -> atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL $ show e) + Right () -> pure () restoreServersStats :: AgentClient -> AM' () restoreServersStats c@AgentClient {smpServersStats, xftpServersStats, srvStatsStartedAt} = do @@ -885,6 +885,7 @@ createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> S createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do let sndSecure = False -- smpClientVersion >= sndAuthKeySMPClientVersion (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode sndSecure + atomically $ incSMPServerStat c userId (qServer rq) connCreated let qInfo = toVersionT qUri smpClientVersion rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId @@ -1175,7 +1176,6 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do ICDeleteRcvQueue rId -> withServer $ \srv -> tryWithLock "ICDeleteRcvQueue" $ do rq <- withStore c (\db -> getDeletedRcvQueue db connId srv rId) deleteQueue c rq - atomically $ incSMPServerStat c userId srv connDeleted withStore' c (`deleteConnRcvQueue` rq) ICQSecure rId senderKey -> withServer $ \srv -> tryWithLock "ICQSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> @@ -1425,7 +1425,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI withStore' c $ \db -> setSndQueueStatus db sq Active case rq_ of -- party initiating connection (in v1) - Just RcvQueue {status} -> + Just rq@RcvQueue {status} -> -- it is unclear why subscribeQueue was needed here, -- message delivery can only be enabled for queues that were created in the current session or subscribed -- subscribeQueue c rq connId @@ -1435,7 +1435,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI -- because it can be sent before HELLO is received -- With `status == Active` condition, CON is sent here only by the accepting party, that previously received HELLO when (status == Active) $ do - atomically $ incSMPServerStat c userId server connCompleted + atomically $ incSMPServerStat c userId (qServer rq) connCompleted notify $ CON pqEncryption -- this branch should never be reached as receive queue is created before the confirmation, _ -> logError "HELLO sent without receive queue" @@ -1627,10 +1627,14 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni _ -> throwE $ CMD PROHIBITED "synchronizeRatchet: not duplex" ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM () -ackQueueMessage c rq srvMsgId = - sendAck c rq srvMsgId `catchAgentError` \case - SMP _ SMP.NO_MSG -> pure () - e -> throwE e +ackQueueMessage c rq@RcvQueue {userId, server} srvMsgId = do + atomically $ incSMPServerStat c userId server ackAttempts + tryAgentError (sendAck c rq srvMsgId) >>= \case + Right _ -> atomically $ incSMPServerStat c userId server ackMsgs + Left (SMP _ SMP.NO_MSG) -> atomically $ incSMPServerStat c userId server ackNoMsgErrs + Left e -> do + unless (temporaryOrHostError e) $ atomically $ incSMPServerStat c userId server ackOtherErrs + throwE e -- | Suspend SMP agent connection (OFF command) in Reader monad suspendConnection' :: AgentClient -> ConnId -> AM () @@ -1727,11 +1731,15 @@ deleteConnQueues c waitDelivery ntf rqs = do Int -> (RcvQueue, Either AgentErrorType ()) -> IO ((RcvQueue, Either AgentErrorType ()), Maybe (AM' ())) - deleteQueueRec db maxErrs (rq, r) = case r of + deleteQueueRec db maxErrs (rq@RcvQueue {userId, server}, r) = case r of Right _ -> deleteConnRcvQueue db rq $> ((rq, r), Just (notifyRQ rq Nothing)) Left e | temporaryOrHostError e && deleteErrors rq + 1 < maxErrs -> incRcvDeleteErrors db rq $> ((rq, r), Nothing) - | otherwise -> deleteConnRcvQueue db rq $> ((rq, Right ()), Just (notifyRQ rq (Just e))) + | otherwise -> do + deleteConnRcvQueue db rq + -- attempts and successes are counted in deleteQueues function + atomically $ incSMPServerStat c userId server connDeleted + pure ((rq, Right ()), Just (notifyRQ rq (Just e))) notifyRQ rq e_ = notify ("", qConnId rq, AEvt SAEConn $ DEL_RCVQ (qServer rq) (queueId rq) e_) notify = when ntf . atomically . writeTBQueue (subQ c) connResults :: [(RcvQueue, Either AgentErrorType ())] -> Map ConnId (Either AgentErrorType ()) @@ -2009,10 +2017,12 @@ setNtfServers c = atomically . writeTVar (ntfServers c) {-# INLINE setNtfServers #-} resetAgentServersStats' :: AgentClient -> AM () -resetAgentServersStats' c@AgentClient {smpServersStats, xftpServersStats} = do +resetAgentServersStats' c@AgentClient {smpServersStats, xftpServersStats, srvStatsStartedAt} = do + startedAt <- liftIO getCurrentTime + atomically $ writeTVar srvStatsStartedAt startedAt atomically $ TM.clear smpServersStats atomically $ TM.clear xftpServersStats - withStore' c resetServersStats + withStore' c (`resetServersStats` startedAt) -- | Activate operations foregroundAgent :: AgentClient -> IO () @@ -2146,7 +2156,7 @@ data ACKd = ACKd | ACKPending -- It cannot be finally, as sometimes it needs to be ACK+DEL, -- and sometimes ACK has to be sent from the consumer. processSMPTransmissions :: AgentClient -> ServerTransmissionBatch SMPVersion ErrorType BrokerMsg -> AM' () -processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) = do +processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), _v, sessId, ts) = do upConnIds <- newTVarIO [] forM_ ts $ \(entId, t) -> case t of STEvent msgOrErr -> @@ -2171,7 +2181,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) logServer "<--" c srv entId $ "error: " <> bshow e notifyErr "" e connIds <- readTVarIO upConnIds - unless (null connIds) $ notify' "" $ UP srv connIds + unless (null connIds) $ do + notify' "" $ UP srv connIds + atomically $ incSMPServerStat' c userId srv connSubscribed $ length connIds where withRcvConn :: SMP.RecipientId -> (forall c. RcvQueue -> Connection c -> AM ()) -> AM' () withRcvConn rId a = do @@ -2182,17 +2194,19 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) Left e -> notify' connId (ERR e) Right () -> pure () processSubOk :: RcvQueue -> TVar [ConnId] -> AM () - processSubOk rq@RcvQueue {userId, connId} upConnIds = do + processSubOk rq@RcvQueue {connId} upConnIds = atomically . whenM (isPendingSub connId) $ do addSubscription c rq modifyTVar' upConnIds (connId :) - atomically $ incSMPServerStat c userId srv connSubscribed processSubErr :: RcvQueue -> SMPClientError -> AM () - processSubErr rq@RcvQueue {userId, connId} e = do - atomically . whenM (isPendingSub connId) $ failSubscription c rq e - atomically $ incSMPServerStat c userId srv connSubErrs + processSubErr rq@RcvQueue {connId} e = do + atomically . whenM (isPendingSub connId) $ + failSubscription c rq e >> incSMPServerStat c userId srv connSubErrs lift $ notifyErr connId e - isPendingSub connId = (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId + isPendingSub connId = do + pending <- (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId + unless pending $ incSMPServerStat c userId srv connSubIgnored + pure pending notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> AEvent e -> m () notify' connId msg = atomically $ writeTBQueue subQ ("", connId, AEvt (sAEntity @e) msg) notifyErr :: ConnId -> SMPClientError -> AM' () @@ -2201,7 +2215,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) processSMP rq@RcvQueue {rcvId = rId, sndSecure, e2ePrivKey, e2eDhSecret, status} conn - cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} + cData@ConnData {connId, connAgentVersion, ratchetSyncState = rss} smpMsg = withConnLock c connId "processSMP" $ case smpMsg of SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> do diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 01d97f9ac..c9db8e7a7 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -142,6 +142,8 @@ module Simplex.Messaging.Agent.Client incSMPServerStat, incSMPServerStat', incXFTPServerStat, + incXFTPServerStat', + incXFTPServerSizeStat, AgentWorkersDetails (..), getAgentWorkersDetails, AgentWorkersSummary (..), @@ -174,8 +176,9 @@ import Data.Bifunctor (bimap, first, second) import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Either (partitionEithers) +import Data.Either (isRight, partitionEithers) import Data.Functor (($>)) +import Data.Int (Int64) import Data.List (deleteFirstsBy, foldl', partition, (\\)) import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L @@ -1329,13 +1332,16 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode sender pure (rq, qUri, tSess, sessId) processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> STM () -processSubResult c rq@RcvQueue {connId} = \case +processSubResult c rq@RcvQueue {userId, server, connId} = \case Left e -> - unless (temporaryClientError e) $ + unless (temporaryClientError e) $ do + incSMPServerStat c userId server connSubErrs failSubscription c rq e Right () -> - whenM (hasPendingSubscription c connId) $ - addSubscription c rq + ifM + (hasPendingSubscription c connId) + (incSMPServerStat c userId server connSubscribed >> addSubscription c rq) + (incSMPServerStat c userId server connSubIgnored) temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case @@ -1387,14 +1393,14 @@ subscribeQueues c qs = do subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) subscribeQueues_ env session smp qs' = do let (userId, srv, _) = transportSession' smp - atomically $ incSMPServerStat' c userId srv connSubAttempts (length qs') + atomically $ incSMPServerStat' c userId srv connSubAttempts $ length qs' rs <- sendBatch subscribeSMPQueues smp qs' active <- atomically $ ifM (activeClientSession c tSess sessId) (writeTVar session (Just sessId) >> processSubResults rs $> True) - (pure False) + (incSMPServerStat' c userId srv connSubIgnored (length rs) $> False) if active then when (hasTempErrors rs) resubscribe $> rs else do @@ -1603,7 +1609,15 @@ deleteQueue c rq@RcvQueue {rcvId, rcvPrivateKey} = do deleteSMPQueue smp rcvPrivateKey rcvId deleteQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] -deleteQueues = sendTSessionBatches "DEL" id $ sendBatch deleteSMPQueues +deleteQueues c = sendTSessionBatches "DEL" id deleteQueues_ c + where + deleteQueues_ smp rqs = do + let (userId, srv, _) = transportSession' smp + atomically $ incSMPServerStat' c userId srv connDelAttempts $ length rqs + rs <- sendBatch deleteSMPQueues smp rqs + let successes = foldl' (\n (_, r) -> if isRight r then n + 1 else n) 0 rs + atomically $ incSMPServerStat' c userId srv connDeleted successes + pure rs sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM (Maybe SMPServer) sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags agentMsg = do @@ -1924,12 +1938,24 @@ incSMPServerStat' AgentClient {smpServersStats} userId srv sel n = do TM.insert (userId, srv) newStats smpServersStats incXFTPServerStat :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int) -> STM () -incXFTPServerStat AgentClient {xftpServersStats} userId srv sel = do +incXFTPServerStat c userId srv sel = incXFTPServerStat_ c userId srv sel 1 +{-# INLINE incXFTPServerStat #-} + +incXFTPServerStat' :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int) -> Int -> STM () +incXFTPServerStat' = incXFTPServerStat_ +{-# INLINE incXFTPServerStat' #-} + +incXFTPServerSizeStat :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int64) -> Int64 -> STM () +incXFTPServerSizeStat = incXFTPServerStat_ +{-# INLINE incXFTPServerSizeStat #-} + +incXFTPServerStat_ :: Num n => AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar n) -> n -> STM () +incXFTPServerStat_ AgentClient {xftpServersStats} userId srv sel n = do TM.lookup (userId, srv) xftpServersStats >>= \case - Just v -> modifyTVar' (sel v) (+ 1) + Just v -> modifyTVar' (sel v) (+ n) Nothing -> do newStats <- newAgentXFTPServerStats - modifyTVar' (sel newStats) (+ 1) + modifyTVar' (sel newStats) (+ n) TM.insert (userId, srv) newStats xftpServersStats data AgentServersSummary = AgentServersSummary diff --git a/src/Simplex/Messaging/Agent/Stats.hs b/src/Simplex/Messaging/Agent/Stats.hs index c8f81a6aa..424052d74 100644 --- a/src/Simplex/Messaging/Agent/Stats.hs +++ b/src/Simplex/Messaging/Agent/Stats.hs @@ -5,6 +5,7 @@ module Simplex.Messaging.Agent.Stats where import qualified Data.Aeson.TH as J +import Data.Int (Int64) import Data.Map (Map) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) @@ -29,12 +30,20 @@ data AgentSMPServerStats = AgentSMPServerStats recvDuplicates :: TVar Int, -- duplicate messages received recvCryptoErrs :: TVar Int, -- message decryption errors recvErrs :: TVar Int, -- receive errors - connCreated :: TVar Int, - connSecured :: TVar Int, - connCompleted :: TVar Int, - connDeleted :: TVar Int, + ackMsgs :: TVar Int, -- total messages acknowledged + ackAttempts :: TVar Int, -- acknowledgement attempts + ackNoMsgErrs :: TVar Int, -- NO_MSG ack errors + ackOtherErrs :: TVar Int, -- other permanent ack errors (temporary accounted for in attempts) + -- conn stats are accounted for rcv queue server + connCreated :: TVar Int, -- total connections created + connSecured :: TVar Int, -- connections secured + connCompleted :: TVar Int, -- connections completed + connDeleted :: TVar Int, -- total connections deleted + connDelAttempts :: TVar Int, -- total connection deletion attempts + connDelErrs :: TVar Int, -- permanent connection deletion errors (temporary accounted for in attempts) connSubscribed :: TVar Int, -- total successful subscription connSubAttempts :: TVar Int, -- subscription attempts + connSubIgnored :: TVar Int, -- subscription results ignored (client switched to different session or it was not pending) connSubErrs :: TVar Int -- permanent subscription errors (temporary accounted for in attempts) } @@ -53,12 +62,19 @@ data AgentSMPServerStatsData = AgentSMPServerStatsData _recvDuplicates :: Int, _recvCryptoErrs :: Int, _recvErrs :: Int, + _ackMsgs :: Int, + _ackAttempts :: Int, + _ackNoMsgErrs :: Int, + _ackOtherErrs :: Int, _connCreated :: Int, _connSecured :: Int, _connCompleted :: Int, _connDeleted :: Int, + _connDelAttempts :: Int, + _connDelErrs :: Int, _connSubscribed :: Int, _connSubAttempts :: Int, + _connSubIgnored :: Int, _connSubErrs :: Int } deriving (Show) @@ -79,12 +95,19 @@ newAgentSMPServerStats = do recvDuplicates <- newTVar 0 recvCryptoErrs <- newTVar 0 recvErrs <- newTVar 0 + ackMsgs <- newTVar 0 + ackAttempts <- newTVar 0 + ackNoMsgErrs <- newTVar 0 + ackOtherErrs <- newTVar 0 connCreated <- newTVar 0 connSecured <- newTVar 0 connCompleted <- newTVar 0 connDeleted <- newTVar 0 + connDelAttempts <- newTVar 0 + connDelErrs <- newTVar 0 connSubscribed <- newTVar 0 connSubAttempts <- newTVar 0 + connSubIgnored <- newTVar 0 connSubErrs <- newTVar 0 pure AgentSMPServerStats @@ -102,15 +125,55 @@ newAgentSMPServerStats = do recvDuplicates, recvCryptoErrs, recvErrs, + ackMsgs, + ackAttempts, + ackNoMsgErrs, + ackOtherErrs, connCreated, connSecured, connCompleted, connDeleted, + connDelAttempts, + connDelErrs, connSubscribed, connSubAttempts, + connSubIgnored, connSubErrs } +newAgentSMPServerStatsData :: AgentSMPServerStatsData +newAgentSMPServerStatsData = + AgentSMPServerStatsData + { _sentDirect = 0, + _sentViaProxy = 0, + _sentProxied = 0, + _sentDirectAttempts = 0, + _sentViaProxyAttempts = 0, + _sentProxiedAttempts = 0, + _sentAuthErrs = 0, + _sentQuotaErrs = 0, + _sentExpiredErrs = 0, + _sentOtherErrs = 0, + _recvMsgs = 0, + _recvDuplicates = 0, + _recvCryptoErrs = 0, + _recvErrs = 0, + _ackMsgs = 0, + _ackAttempts = 0, + _ackNoMsgErrs = 0, + _ackOtherErrs = 0, + _connCreated = 0, + _connSecured = 0, + _connCompleted = 0, + _connDeleted = 0, + _connDelAttempts = 0, + _connDelErrs = 0, + _connSubscribed = 0, + _connSubAttempts = 0, + _connSubIgnored = 0, + _connSubErrs = 0 + } + newAgentSMPServerStats' :: AgentSMPServerStatsData -> STM AgentSMPServerStats newAgentSMPServerStats' s = do sentDirect <- newTVar $ _sentDirect s @@ -127,12 +190,19 @@ newAgentSMPServerStats' s = do recvDuplicates <- newTVar $ _recvDuplicates s recvCryptoErrs <- newTVar $ _recvCryptoErrs s recvErrs <- newTVar $ _recvErrs s + ackMsgs <- newTVar $ _ackMsgs s + ackAttempts <- newTVar $ _ackAttempts s + ackNoMsgErrs <- newTVar $ _ackNoMsgErrs s + ackOtherErrs <- newTVar $ _ackOtherErrs s connCreated <- newTVar $ _connCreated s connSecured <- newTVar $ _connSecured s connCompleted <- newTVar $ _connCompleted s connDeleted <- newTVar $ _connDeleted s + connDelAttempts <- newTVar $ _connDelAttempts s + connDelErrs <- newTVar $ _connDelErrs s connSubscribed <- newTVar $ _connSubscribed s connSubAttempts <- newTVar $ _connSubAttempts s + connSubIgnored <- newTVar $ _connSubIgnored s connSubErrs <- newTVar $ _connSubErrs s pure AgentSMPServerStats @@ -150,12 +220,19 @@ newAgentSMPServerStats' s = do recvDuplicates, recvCryptoErrs, recvErrs, + ackMsgs, + ackAttempts, + ackNoMsgErrs, + ackOtherErrs, connCreated, connSecured, connCompleted, connDeleted, + connDelAttempts, + connDelErrs, connSubscribed, connSubAttempts, + connSubIgnored, connSubErrs } @@ -177,12 +254,19 @@ getAgentSMPServerStats s = do _recvDuplicates <- readTVarIO $ recvDuplicates s _recvCryptoErrs <- readTVarIO $ recvCryptoErrs s _recvErrs <- readTVarIO $ recvErrs s + _ackMsgs <- readTVarIO $ ackMsgs s + _ackAttempts <- readTVarIO $ ackAttempts s + _ackNoMsgErrs <- readTVarIO $ ackNoMsgErrs s + _ackOtherErrs <- readTVarIO $ ackOtherErrs s _connCreated <- readTVarIO $ connCreated s _connSecured <- readTVarIO $ connSecured s _connCompleted <- readTVarIO $ connCompleted s _connDeleted <- readTVarIO $ connDeleted s + _connDelAttempts <- readTVarIO $ connDelAttempts s + _connDelErrs <- readTVarIO $ connDelErrs s _connSubscribed <- readTVarIO $ connSubscribed s _connSubAttempts <- readTVarIO $ connSubAttempts s + _connSubIgnored <- readTVarIO $ connSubIgnored s _connSubErrs <- readTVarIO $ connSubErrs s pure AgentSMPServerStatsData @@ -200,20 +284,62 @@ getAgentSMPServerStats s = do _recvDuplicates, _recvCryptoErrs, _recvErrs, + _ackMsgs, + _ackAttempts, + _ackNoMsgErrs, + _ackOtherErrs, _connCreated, _connSecured, _connCompleted, _connDeleted, + _connDelAttempts, + _connDelErrs, _connSubscribed, _connSubAttempts, + _connSubIgnored, _connSubErrs } +addSMPStatsData :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData +addSMPStatsData sd1 sd2 = + AgentSMPServerStatsData + { _sentDirect = _sentDirect sd1 + _sentDirect sd2, + _sentViaProxy = _sentViaProxy sd1 + _sentViaProxy sd2, + _sentProxied = _sentProxied sd1 + _sentProxied sd2, + _sentDirectAttempts = _sentDirectAttempts sd1 + _sentDirectAttempts sd2, + _sentViaProxyAttempts = _sentViaProxyAttempts sd1 + _sentViaProxyAttempts sd2, + _sentProxiedAttempts = _sentProxiedAttempts sd1 + _sentProxiedAttempts sd2, + _sentAuthErrs = _sentAuthErrs sd1 + _sentAuthErrs sd2, + _sentQuotaErrs = _sentQuotaErrs sd1 + _sentQuotaErrs sd2, + _sentExpiredErrs = _sentExpiredErrs sd1 + _sentExpiredErrs sd2, + _sentOtherErrs = _sentOtherErrs sd1 + _sentOtherErrs sd2, + _recvMsgs = _recvMsgs sd1 + _recvMsgs sd2, + _recvDuplicates = _recvDuplicates sd1 + _recvDuplicates sd2, + _recvCryptoErrs = _recvCryptoErrs sd1 + _recvCryptoErrs sd2, + _recvErrs = _recvErrs sd1 + _recvErrs sd2, + _ackMsgs = _ackMsgs sd1 + _ackMsgs sd2, + _ackAttempts = _ackAttempts sd1 + _ackAttempts sd2, + _ackNoMsgErrs = _ackNoMsgErrs sd1 + _ackNoMsgErrs sd2, + _ackOtherErrs = _ackOtherErrs sd1 + _ackOtherErrs sd2, + _connCreated = _connCreated sd1 + _connCreated sd2, + _connSecured = _connSecured sd1 + _connSecured sd2, + _connCompleted = _connCompleted sd1 + _connCompleted sd2, + _connDeleted = _connDeleted sd1 + _connDeleted sd2, + _connDelAttempts = _connDelAttempts sd1 + _connDelAttempts sd2, + _connDelErrs = _connDelErrs sd1 + _connDelErrs sd2, + _connSubscribed = _connSubscribed sd1 + _connSubscribed sd2, + _connSubAttempts = _connSubAttempts sd1 + _connSubAttempts sd2, + _connSubIgnored = _connSubIgnored sd1 + _connSubIgnored sd2, + _connSubErrs = _connSubErrs sd1 + _connSubErrs sd2 + } + data AgentXFTPServerStats = AgentXFTPServerStats { uploads :: TVar Int, -- total replicas uploaded to server + uploadsSize :: TVar Int64, -- total size of uploaded replicas in KB uploadAttempts :: TVar Int, -- upload attempts uploadErrs :: TVar Int, -- upload errors downloads :: TVar Int, -- total replicas downloaded from server + downloadsSize :: TVar Int64, -- total size of downloaded replicas in KB downloadAttempts :: TVar Int, -- download attempts downloadAuthErrs :: TVar Int, -- download AUTH errors downloadErrs :: TVar Int, -- other download errors (excluding above) @@ -224,9 +350,11 @@ data AgentXFTPServerStats = AgentXFTPServerStats data AgentXFTPServerStatsData = AgentXFTPServerStatsData { _uploads :: Int, + _uploadsSize :: Int64, _uploadAttempts :: Int, _uploadErrs :: Int, _downloads :: Int, + _downloadsSize :: Int64, _downloadAttempts :: Int, _downloadAuthErrs :: Int, _downloadErrs :: Int, @@ -239,9 +367,11 @@ data AgentXFTPServerStatsData = AgentXFTPServerStatsData newAgentXFTPServerStats :: STM AgentXFTPServerStats newAgentXFTPServerStats = do uploads <- newTVar 0 + uploadsSize <- newTVar 0 uploadAttempts <- newTVar 0 uploadErrs <- newTVar 0 downloads <- newTVar 0 + downloadsSize <- newTVar 0 downloadAttempts <- newTVar 0 downloadAuthErrs <- newTVar 0 downloadErrs <- newTVar 0 @@ -251,9 +381,11 @@ newAgentXFTPServerStats = do pure AgentXFTPServerStats { uploads, + uploadsSize, uploadAttempts, uploadErrs, downloads, + downloadsSize, downloadAttempts, downloadAuthErrs, downloadErrs, @@ -262,12 +394,31 @@ newAgentXFTPServerStats = do deleteErrs } +newAgentXFTPServerStatsData :: AgentXFTPServerStatsData +newAgentXFTPServerStatsData = + AgentXFTPServerStatsData + { _uploads = 0, + _uploadsSize = 0, + _uploadAttempts = 0, + _uploadErrs = 0, + _downloads = 0, + _downloadsSize = 0, + _downloadAttempts = 0, + _downloadAuthErrs = 0, + _downloadErrs = 0, + _deletions = 0, + _deleteAttempts = 0, + _deleteErrs = 0 + } + newAgentXFTPServerStats' :: AgentXFTPServerStatsData -> STM AgentXFTPServerStats newAgentXFTPServerStats' s = do uploads <- newTVar $ _uploads s + uploadsSize <- newTVar $ _uploadsSize s uploadAttempts <- newTVar $ _uploadAttempts s uploadErrs <- newTVar $ _uploadErrs s downloads <- newTVar $ _downloads s + downloadsSize <- newTVar $ _downloadsSize s downloadAttempts <- newTVar $ _downloadAttempts s downloadAuthErrs <- newTVar $ _downloadAuthErrs s downloadErrs <- newTVar $ _downloadErrs s @@ -277,9 +428,11 @@ newAgentXFTPServerStats' s = do pure AgentXFTPServerStats { uploads, + uploadsSize, uploadAttempts, uploadErrs, downloads, + downloadsSize, downloadAttempts, downloadAuthErrs, downloadErrs, @@ -293,9 +446,11 @@ newAgentXFTPServerStats' s = do getAgentXFTPServerStats :: AgentXFTPServerStats -> IO AgentXFTPServerStatsData getAgentXFTPServerStats s = do _uploads <- readTVarIO $ uploads s + _uploadsSize <- readTVarIO $ uploadsSize s _uploadAttempts <- readTVarIO $ uploadAttempts s _uploadErrs <- readTVarIO $ uploadErrs s _downloads <- readTVarIO $ downloads s + _downloadsSize <- readTVarIO $ downloadsSize s _downloadAttempts <- readTVarIO $ downloadAttempts s _downloadAuthErrs <- readTVarIO $ downloadAuthErrs s _downloadErrs <- readTVarIO $ downloadErrs s @@ -305,9 +460,11 @@ getAgentXFTPServerStats s = do pure AgentXFTPServerStatsData { _uploads, + _uploadsSize, _uploadAttempts, _uploadErrs, _downloads, + _downloadsSize, _downloadAttempts, _downloadAuthErrs, _downloadErrs, @@ -316,6 +473,23 @@ getAgentXFTPServerStats s = do _deleteErrs } +addXFTPStatsData :: AgentXFTPServerStatsData -> AgentXFTPServerStatsData -> AgentXFTPServerStatsData +addXFTPStatsData sd1 sd2 = + AgentXFTPServerStatsData + { _uploads = _uploads sd1 + _uploads sd2, + _uploadsSize = _uploadsSize sd1 + _uploadsSize sd2, + _uploadAttempts = _uploadAttempts sd1 + _uploadAttempts sd2, + _uploadErrs = _uploadErrs sd1 + _uploadErrs sd2, + _downloads = _downloads sd1 + _downloads sd2, + _downloadsSize = _downloadsSize sd1 + _downloadsSize sd2, + _downloadAttempts = _downloadAttempts sd1 + _downloadAttempts sd2, + _downloadAuthErrs = _downloadAuthErrs sd1 + _downloadAuthErrs sd2, + _downloadErrs = _downloadErrs sd1 + _downloadErrs sd2, + _deletions = _deletions sd1 + _deletions sd2, + _deleteAttempts = _deleteAttempts sd1 + _deleteAttempts sd2, + _deleteErrs = _deleteErrs sd1 + _deleteErrs sd2 + } + -- Type for gathering both smp and xftp stats across all users and servers, -- to then be persisted to db as a single json. data AgentPersistedServerStats = AgentPersistedServerStats diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index baec2ef93..ae010a884 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -175,6 +175,12 @@ instance SMPQueue RcvQueue where queueId RcvQueue {rcvId} = rcvId {-# INLINE queueId #-} +instance SMPQueue NewRcvQueue where + qServer RcvQueue {server} = server + {-# INLINE qServer #-} + queueId RcvQueue {rcvId} = rcvId + {-# INLINE queueId #-} + instance SMPQueue SndQueue where qServer SndQueue {server} = server {-# INLINE qServer #-} diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index d4cd99b39..0727343e7 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -3041,10 +3041,9 @@ getServersStats db = firstRow id SEServersStatsNotFound $ DB.query_ db "SELECT started_at, servers_stats FROM servers_stats WHERE servers_stats_id = 1" -resetServersStats :: DB.Connection -> IO () -resetServersStats db = do - currentTs <- getCurrentTime - DB.execute db "UPDATE servers_stats SET servers_stats = NULL, started_at = ?, updated_at = ? WHERE servers_stats_id = 1" (currentTs, currentTs) +resetServersStats :: DB.Connection -> UTCTime -> IO () +resetServersStats db startedAt = + DB.execute db "UPDATE servers_stats SET servers_stats = NULL, started_at = ?, updated_at = ? WHERE servers_stats_id = 1" (startedAt, startedAt) $(J.deriveJSON defaultJSON ''UpMigration) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index 2279d7ea5..131561f4d 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -73,6 +73,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wai import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure +import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Transport.Client (TransportHost) @@ -114,7 +115,8 @@ schemaMigrations = ("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery), ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem), ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays), - ("m20240624_snd_secure", m20240624_snd_secure, Just down_m20240624_snd_secure) + ("m20240624_snd_secure", m20240624_snd_secure, Just down_m20240624_snd_secure), + ("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240702_servers_stats.hs similarity index 79% rename from src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs rename to src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240702_servers_stats.hs index fe017e233..5e283d8b1 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240518_servers_stats.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240702_servers_stats.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats where +module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) @@ -8,8 +8,8 @@ import Database.SQLite.Simple.QQ (sql) -- servers_stats_id: dummy id, there should always only be one record with servers_stats_id = 1 -- servers_stats: overall accumulated stats, past and session, reset to null on stats reset -- started_at: starting point of tracking stats, reset on stats reset -m20240518_servers_stats :: Query -m20240518_servers_stats = +m20240702_servers_stats :: Query +m20240702_servers_stats = [sql| CREATE TABLE servers_stats( servers_stats_id INTEGER PRIMARY KEY, @@ -22,8 +22,8 @@ CREATE TABLE servers_stats( INSERT INTO servers_stats (servers_stats_id) VALUES (1); |] -down_m20240518_servers_stats :: Query -down_m20240518_servers_stats = +down_m20240702_servers_stats :: Query +down_m20240702_servers_stats = [sql| DROP TABLE servers_stats; |] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index b9d2d945f..80af08989 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -396,6 +396,13 @@ CREATE TABLE processed_ratchet_key_hashes( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); +CREATE TABLE servers_stats( + servers_stats_id INTEGER PRIMARY KEY, + servers_stats TEXT, + started_at TEXT NOT NULL DEFAULT(datetime('now')), + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id); CREATE UNIQUE INDEX idx_rcv_queue_id ON rcv_queues(conn_id, rcv_queue_id); CREATE UNIQUE INDEX idx_snd_queue_id ON snd_queues(conn_id, snd_queue_id); From ce732c0efbc5530035e7b7042e398c1b9ea6e4fe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 3 Jul 2024 18:05:27 +0100 Subject: [PATCH 112/125] agent: enable fast handshake (revert #1215) (#1216) * Revert "agent: disable fast handshake (#1215)" This reverts commit aa1d8d6c8bcff82f02aa580b1b729fc4b1396fd9. * remove import * test delays --- src/Simplex/Messaging/Agent.hs | 5 +- tests/AgentTests/FunctionalAPITests.hs | 111 +++++++++++++------------ tests/AgentTests/NotificationTests.hs | 22 ++--- tests/SMPProxyTests.hs | 18 ++-- 4 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 480c8f801..7764d16a3 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -157,7 +157,6 @@ import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Stats -import Simplex.Messaging.Agent.Stats (AgentSMPServerStats (connSubErrs)) import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -739,7 +738,7 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - let sndSecure = False -- case cMode of SCMInvitation -> True; SCMContact -> False + let sndSecure = case cMode of SCMInvitation -> True; SCMContact -> False (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srvWithAuth smpClientVRange subMode sndSecure `catchAgentError` \e -> liftIO (print e) >> throwE e atomically $ incSMPServerStat c userId srv connCreated rq' <- withStore c $ \db -> updateNewConnRcv db connId rq @@ -883,7 +882,7 @@ joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do - let sndSecure = False -- smpClientVersion >= sndAuthKeySMPClientVersion + let sndSecure = smpClientVersion >= sndAuthKeySMPClientVersion (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode sndSecure atomically $ incSMPServerStat c userId (qServer rq) connCreated let qInfo = toVersionT qUri smpClientVersion diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 19f4977fc..569901c77 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -257,11 +257,11 @@ functionalAPITests :: ATransport -> Spec functionalAPITests t = do describe "Establishing duplex connection" $ do testMatrix2 t runAgentClientTest - xit "should connect when server with multiple identities is stored" $ + it "should connect when server with multiple identities is stored" $ withSmpServer t testServerMultipleIdentities - xit "should connect with two peers" $ + it "should connect with two peers" $ withSmpServer t testAgentClient3 - xit "should establish connection without PQ encryption and enable it" $ + it "should establish connection without PQ encryption and enable it" $ withSmpServer t testEnablePQEncryption describe "Establishing duplex connection, different PQ settings" $ do testPQMatrix2 t $ runAgentClientTestPQ True @@ -290,43 +290,43 @@ functionalAPITests t = do testAllowConnectionClientRestart t describe "Message delivery" $ do describe "update connection agent version on received messages" $ do - xit "should increase if compatible, shouldn't decrease" $ + it "should increase if compatible, shouldn't decrease" $ testIncreaseConnAgentVersion t - xit "should increase to max compatible version" $ + it "should increase to max compatible version" $ testIncreaseConnAgentVersionMaxCompatible t - xit "should increase when connection was negotiated on different versions" $ + it "should increase when connection was negotiated on different versions" $ testIncreaseConnAgentVersionStartDifferentVersion t -- TODO PQ tests for upgrading connection to PQ encryption - xit "should deliver message after client restart" $ + it "should deliver message after client restart" $ testDeliverClientRestart t - xit "should deliver messages to the user once, even if repeat delivery is made by the server (no ACK)" $ + it "should deliver messages to the user once, even if repeat delivery is made by the server (no ACK)" $ testDuplicateMessage t - xit "should report error via msg integrity on skipped messages" $ + it "should report error via msg integrity on skipped messages" $ testSkippedMessages t - xit "should connect to the server when server goes up if it initially was down" $ + it "should connect to the server when server goes up if it initially was down" $ testDeliveryAfterSubscriptionError t - xit "should deliver messages if one of connections has quota exceeded" $ + it "should deliver messages if one of connections has quota exceeded" $ testMsgDeliveryQuotaExceeded t describe "message expiration" $ do - xit "should expire one message" $ testExpireMessage t - xit "should expire multiple messages" $ testExpireManyMessages t - xit "should expire one message if quota is exceeded" $ testExpireMessageQuota t - xit "should expire multiple messages if quota is exceeded" $ testExpireManyMessagesQuota t + it "should expire one message" $ testExpireMessage t + it "should expire multiple messages" $ testExpireManyMessages t + it "should expire one message if quota is exceeded" $ testExpireMessageQuota t + it "should expire multiple messages if quota is exceeded" $ testExpireManyMessagesQuota t describe "Ratchet synchronization" $ do - xit "should report ratchet de-synchronization, synchronize ratchets" $ + it "should report ratchet de-synchronization, synchronize ratchets" $ testRatchetSync t - xit "should synchronize ratchets after server being offline" $ + it "should synchronize ratchets after server being offline" $ testRatchetSyncServerOffline t - xit "should synchronize ratchets after client restart" $ + it "should synchronize ratchets after client restart" $ testRatchetSyncClientRestart t - xit "should synchronize ratchets after suspend/foreground" $ + it "should synchronize ratchets after suspend/foreground" $ testRatchetSyncSuspendForeground t - xit "should synchronize ratchets when clients start synchronization simultaneously" $ + it "should synchronize ratchets when clients start synchronization simultaneously" $ testRatchetSyncSimultaneous t describe "Subscription mode OnlyCreate" $ do it "messages delivered only when polled (v8 - slow handshake)" $ withSmpServer t testOnlyCreatePullSlowHandshake - xit "messages delivered only when polled" $ + it "messages delivered only when polled" $ withSmpServer t testOnlyCreatePull describe "Inactive client disconnection" $ do it "should disconnect clients without subs if they were inactive longer than TTL" $ @@ -336,14 +336,14 @@ functionalAPITests t = do it "should NOT disconnect active clients" $ testActiveClientNotDisconnected t describe "Suspending agent" $ do - xit "should update client when agent is suspended" $ + it "should update client when agent is suspended" $ withSmpServer t testSuspendingAgent - xit "should complete sending messages when agent is suspended" $ + it "should complete sending messages when agent is suspended" $ testSuspendingAgentCompleteSending t - xit "should suspend agent on timeout, even if pending messages not sent" $ + it "should suspend agent on timeout, even if pending messages not sent" $ testSuspendingAgentTimeout t describe "Batching SMP commands" $ do - xit "should subscribe to multiple (200) subscriptions with batching" $ + it "should subscribe to multiple (200) subscriptions with batching" $ testBatchedSubscriptions 200 10 t skip "faster version of the previous test (200 subscriptions gets very slow with test coverage)" $ it "should subscribe to multiple (6) subscriptions with batching" $ @@ -362,7 +362,7 @@ functionalAPITests t = do testDeleteConnectionAsync t it "join connection when reply queue creation fails (v8 - slow handshake)" $ testJoinConnectionAsyncReplyErrorV8 t - xit "join connection when reply queue creation fails" $ + it "join connection when reply queue creation fails" $ testJoinConnectionAsyncReplyError t describe "delete connection waiting for delivery" $ do it "should delete connection immediately if there are no pending messages" $ @@ -376,34 +376,34 @@ functionalAPITests t = do it "should delete connection by timeout, message in progress can be delivered" $ testWaitDeliveryTimeout2 t describe "Users" $ do - xit "should create and delete user with connections" $ + it "should create and delete user with connections" $ withSmpServer t testUsers - xit "should create and delete user without connections" $ + it "should create and delete user without connections" $ withSmpServer t testDeleteUserQuietly - xit "should create and delete user with connections when server connection fails" $ + it "should create and delete user with connections when server connection fails" $ testUsersNoServer t - xit "should connect two users and switch session mode" $ + it "should connect two users and switch session mode" $ withSmpServer t testTwoUsers describe "Connection switch" $ do - xdescribe "should switch delivery to the new queue" $ + describe "should switch delivery to the new queue" $ testServerMatrix2 t testSwitchConnection - xdescribe "should switch to new queue asynchronously" $ + describe "should switch to new queue asynchronously" $ testServerMatrix2 t testSwitchAsync describe "should delete connection during switch" $ testServerMatrix2 t testSwitchDelete - xdescribe "should abort switch in Started phase" $ + describe "should abort switch in Started phase" $ testServerMatrix2 t testAbortSwitchStarted - xdescribe "should abort switch in Started phase, reinitiate immediately" $ + describe "should abort switch in Started phase, reinitiate immediately" $ testServerMatrix2 t testAbortSwitchStartedReinitiate - xdescribe "should prohibit to abort switch in Secured phase" $ + describe "should prohibit to abort switch in Secured phase" $ testServerMatrix2 t testCannotAbortSwitchSecured - xdescribe "should switch two connections simultaneously" $ + describe "should switch two connections simultaneously" $ testServerMatrix2 t testSwitch2Connections - xdescribe "should switch two connections simultaneously, abort one" $ + describe "should switch two connections simultaneously, abort one" $ testServerMatrix2 t testSwitch2ConnectionsAbort1 describe "SMP basic auth" $ do let v4 = prevVersion basicAuthSMPVersion - forM_ (nub [prevVersion authCmdsSMPVersion, authCmdsSMPVersion, prevVersion currentServerSMPRelayVersion]) $ \v -> do + forM_ (nub [prevVersion authCmdsSMPVersion, authCmdsSMPVersion, currentServerSMPRelayVersion]) $ \v -> do let baseId = if v >= sndAuthKeySMPVersion then 1 else 3 describe ("v" <> show v <> ": with server auth") $ do -- allow NEW | server auth, v | clnt1 auth, v | clnt2 auth, v | 2 - success, 1 - JOIN fail, 0 - NEW fail @@ -444,15 +444,15 @@ functionalAPITests t = do it "should return the same data for both peers" $ withSmpServer t testRatchetAdHash describe "Delivery receipts" $ do - xit "should send and receive delivery receipt" $ withSmpServer t testDeliveryReceipts - xit "should send delivery receipt only in connection v3+" $ testDeliveryReceiptsVersion t + it "should send and receive delivery receipt" $ withSmpServer t testDeliveryReceipts + it "should send delivery receipt only in connection v3+" $ testDeliveryReceiptsVersion t it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent t describe "user network info" $ do it "should wait for user network" testWaitForUserNetwork it "should not reset online to offline if happens too quickly" testDoNotResetOnlineToOffline it "should resume multiple threads" testResumeMultipleThreads describe "SMP queue info" $ do - xit "server should respond with queue and subscription information" $ + it "server should respond with queue and subscription information" $ withSmpServer t testServerQueueInfo testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> AgentMsgId -> IO Int @@ -475,28 +475,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = testMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - xit "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True - xit "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False testBasicMatrix2 :: HasCallStack => ATransport -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testBasicMatrix2 t runTest = do - xit "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrevPQ agentCfgVPrevPQ 3 $ runTest it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrevPQ agentCfg 3 $ runTest it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrevPQ 3 $ runTest testRatchetMatrix2 :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - xit "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentCfg agentCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfgV8 agentProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True - xit "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False - xit "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 1 $ runTest PQSupportOff False - xit "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 1 $ runTest PQSupportOff False - xit "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 1 $ runTest PQSupportOff False + it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn False + it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 1 $ runTest PQSupportOff False + it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 1 $ runTest PQSupportOff False + it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 1 $ runTest PQSupportOff False testServerMatrix2 :: HasCallStack => ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -911,6 +911,7 @@ testAllowConnectionClientRestart t = do threadDelay 100000 -- give time to enqueue confirmation (enqueueConfirmation) disposeAgentClient alice + threadDelay 250000 alice2 <- getSMPAgentClient' 3 agentCfg initAgentServers testDB @@ -1947,7 +1948,7 @@ testWaitDeliveryNoPending t = withAgentClients2 $ \alice bob -> liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDelivery :: ATransport -> IO () @@ -2001,7 +2002,7 @@ testWaitDelivery t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDeliveryAUTHErr :: ATransport -> IO () @@ -2044,7 +2045,7 @@ testWaitDeliveryAUTHErr t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDeliveryTimeout :: ATransport -> IO () @@ -2084,7 +2085,7 @@ testWaitDeliveryTimeout t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testWaitDeliveryTimeout2 :: ATransport -> IO () @@ -2130,7 +2131,7 @@ testWaitDeliveryTimeout2 t = liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" where - baseId = 3 + baseId = 1 msgId = subtract baseId testJoinConnectionAsyncReplyErrorV8 :: HasCallStack => ATransport -> IO () @@ -3087,7 +3088,7 @@ exchangeGreetings :: HasCallStack => AgentClient -> ConnId -> AgentClient -> Con exchangeGreetings = exchangeGreetings_ PQEncOn exchangeGreetings_ :: HasCallStack => PQEncryption -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () -exchangeGreetings_ pqEnc = exchangeGreetingsMsgId_ pqEnc 4 +exchangeGreetings_ pqEnc = exchangeGreetingsMsgId_ pqEnc 2 exchangeGreetingsMsgId :: HasCallStack => Int64 -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetingsMsgId = exchangeGreetingsMsgId_ PQEncOn diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 603ffd3c0..f5988672d 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -105,7 +105,7 @@ notificationTests t = do describe "Managing notification subscriptions" $ do describe "should create notification subscription for existing connection" $ testNtfMatrix t testNotificationSubscriptionExistingConnection - xdescribe "should create notification subscription for new connection" $ + describe "should create notification subscription for new connection" $ testNtfMatrix t testNotificationSubscriptionNewConnection it "should change notifications mode" $ withSmpServer t $ @@ -116,19 +116,19 @@ notificationTests t = do withAPNSMockServer $ \apns -> withNtfServer t $ testChangeToken apns describe "Notifications server store log" $ - xit "should save and restore tokens and subscriptions" $ + it "should save and restore tokens and subscriptions" $ withSmpServer t $ withAPNSMockServer $ \apns -> testNotificationsStoreLog t apns describe "Notifications after SMP server restart" $ - xit "should resume subscriptions after SMP server is restarted" $ + it "should resume subscriptions after SMP server is restarted" $ withAPNSMockServer $ \apns -> withNtfServer t $ testNotificationsSMPRestart t apns describe "Notifications after SMP server restart" $ it "should resume batched subscriptions after SMP server is restarted" $ withAPNSMockServer $ \apns -> withNtfServer t $ testNotificationsSMPRestartBatch 100 t apns - xdescribe "should switch notifications to the new queue" $ + describe "should switch notifications to the new queue" $ testServerMatrix2 t $ \servers -> withAPNSMockServer $ \apns -> withNtfServer t $ testSwitchNotifications servers apns @@ -146,7 +146,7 @@ notificationTests t = do testNtfMatrix :: HasCallStack => ATransport -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> Spec testNtfMatrix t runTest = do describe "next and current" $ do - xit "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest + it "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest it "curr servers; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest it "prev servers; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest it "prev servers; curr clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest @@ -374,20 +374,20 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} baseId ali -- alice client already has subscription for the connection Left (CMD PROHIBITED _) <- runExceptT $ getNotificationMessage alice nonce message - threadDelay 200000 + threadDelay 300000 suspendAgent alice 0 closeSQLiteStore store - threadDelay 200000 + threadDelay 300000 -- aliceNtf client doesn't have subscription and is allowed to get notification message withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do (_, [SMPMsgMeta {msgFlags = MsgFlags True}]) <- getNotificationMessage aliceNtf nonce message pure () - threadDelay 200000 + threadDelay 300000 reopenSQLiteStore store foregroundAgent alice - threadDelay 200000 + threadDelay 300000 runRight_ $ do get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False @@ -515,7 +515,7 @@ testChangeNotificationsMode APNSMockServer {apnsQ} = -- no notifications should follow noNotification apnsQ where - baseId = 3 + baseId = 1 msgId = subtract baseId testChangeToken :: APNSMockServer -> IO () @@ -554,7 +554,7 @@ testChangeToken APNSMockServer {apnsQ} = withAgent 1 agentCfg initAgentServers t -- no notifications should follow noNotification apnsQ where - baseId = 3 + baseId = 1 msgId = subtract baseId testNotificationsStoreLog :: ATransport -> APNSMockServer -> IO () diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index e05ff884d..6452d2677 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -101,26 +101,26 @@ smpProxyTests = do it "500x20" . twoServersFirstProxy $ 500 `inParrallel` deliver 20 describe "agent API" $ do describe "one server" $ do - xit "always via proxy" . oneServer $ + it "always via proxy" . oneServer $ agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1 - xit "without proxy" . oneServer $ + it "without proxy" . oneServer $ agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 describe "two servers" $ do - xit "always via proxy" . twoServers $ + it "always via proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1 - xit "both via proxy" . twoServers $ + it "both via proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" 1 - xit "first via proxy" . twoServers $ + it "first via proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 - xit "without proxy" . twoServers $ + it "without proxy" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1 - xit "first via proxy for unknown" . twoServers $ + it "first via proxy for unknown" . twoServers $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 1 it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $ agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 3 it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ agentViaProxyVersionError - xit "retries sending when destination or proxy relay is offline" $ + it "retries sending when destination or proxy relay is offline" $ agentViaProxyRetryOffline describe "stress test 1k" $ do let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) @@ -370,7 +370,7 @@ agentViaProxyRetryOffline = do a `up` cId = nGet a =##> \case ("", "", UP _ [c]) -> c == cId; _ -> False a `down` cId = nGet a =##> \case ("", "", DOWN _ [c]) -> c == cId; _ -> False aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval} - baseId = 3 + baseId = 1 msgId = subtract baseId . fst servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]} From 9d0774a58e3307892e0b38500fe96fb5d15d9def Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 3 Jul 2024 19:32:27 +0100 Subject: [PATCH 113/125] agent: add queue information (#1217) * agent: add queue information to "debug delivery" response * fix test * rename * encodings --- src/Simplex/Messaging/Agent.hs | 5 ++--- src/Simplex/Messaging/Agent/Client.hs | 29 ++++++++++++++++++++------ tests/AgentTests/FunctionalAPITests.hs | 4 ++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 7764d16a3..c7b5f5390 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -174,7 +174,6 @@ import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth, sndAuthKeySMPClientVersion) import qualified Simplex.Messaging.Protocol as SMP -import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (SMPVersion, THandleParams (sessionId)) @@ -403,7 +402,7 @@ ackMessage :: AgentClient -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AE ackMessage c = withAgentEnv c .:. ackMessage' c {-# INLINE ackMessage #-} -getConnectionQueueInfo :: AgentClient -> ConnId -> AE QueueInfo +getConnectionQueueInfo :: AgentClient -> ConnId -> AE ServerQueueInfo getConnectionQueueInfo c = withAgentEnv c . getConnectionQueueInfo' c {-# INLINE getConnectionQueueInfo #-} @@ -1542,7 +1541,7 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do withStore' c $ \db -> deleteDeliveredSndMsg db connId $ InternalId sndMsgId _ -> pure () -getConnectionQueueInfo' :: AgentClient -> ConnId -> AM QueueInfo +getConnectionQueueInfo' :: AgentClient -> ConnId -> AM ServerQueueInfo getConnectionQueueInfo' c connId = do SomeConn _ conn <- withStore c (`getConn` connId) case conn of diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index c9db8e7a7..0467c31f8 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -89,6 +89,7 @@ module Simplex.Messaging.Agent.Client activeClientSession, agentClientStore, agentDRG, + ServerQueueInfo (..), AgentServersSummary (..), ServerSessions (..), SMPServerSubs (..), @@ -173,7 +174,7 @@ import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import qualified Data.Aeson.TH as J import Data.Bifunctor (bimap, first, second) -import Data.ByteString.Base64 +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (isRight, partitionEithers) @@ -1505,7 +1506,7 @@ showServer ProtocolServer {host, port} = {-# INLINE showServer #-} logSecret :: ByteString -> ByteString -logSecret bs = encode $ B.take 3 bs +logSecret bs = B64.encode $ B.take 3 bs {-# INLINE logSecret #-} sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM (Maybe SMPServer) @@ -1625,10 +1626,24 @@ sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags a msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg sendOrProxySMPMessage c userId server "" (Just sndPrivateKey) sndId msgFlags msg -getQueueInfo :: AgentClient -> RcvQueue -> AM QueueInfo -getQueueInfo c rq@RcvQueue {rcvId, rcvPrivateKey} = - withSMPClient c rq "QUE" $ \smp -> - getSMPQueueInfo smp rcvPrivateKey rcvId +data ServerQueueInfo = ServerQueueInfo + { server :: SMPServer, + rcvId :: Text, + sndId :: Text, + ntfId :: Maybe Text, + status :: Text, + info :: QueueInfo + } + deriving (Show) + +getQueueInfo :: AgentClient -> RcvQueue -> AM ServerQueueInfo +getQueueInfo c rq@RcvQueue {server, rcvId, rcvPrivateKey, sndId, status, clientNtfCreds} = + withSMPClient c rq "QUE" $ \smp -> do + info <- getSMPQueueInfo smp rcvPrivateKey rcvId + let ntfId = enc . (\ClientNtfCreds {notifierId} -> notifierId) <$> clientNtfCreds + pure ServerQueueInfo {server, rcvId = enc rcvId, sndId = enc sndId, ntfId, status = serializeQueueStatus status, info} + where + enc = decodeLatin1 . B64.encode agentNtfRegisterToken :: AgentClient -> NtfToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519) agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey = @@ -2249,3 +2264,5 @@ $(J.deriveJSON defaultJSON ''AgentQueuesInfo) $(J.deriveJSON (enumJSON $ dropPrefix "UN") ''UserNetworkType) $(J.deriveJSON defaultJSON ''UserNetworkInfo) + +$(J.deriveJSON defaultJSON ''ServerQueueInfo) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 569901c77..9cf7c2793 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -78,7 +78,7 @@ import SMPAgentClient import SMPClient (cfg, prevRange, prevVersion, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import qualified Simplex.Messaging.Agent as A -import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork) +import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), ServerQueueInfo (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore) import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A @@ -3057,7 +3057,7 @@ testServerQueueInfo = do liftIO $ isJust r `shouldBe` True pure r checkQ c cId qiSnd' qiSubThread_ qiSize' msgType_ = do - QueueInfo {qiSnd, qiNtf, qiSub, qiSize, qiMsg} <- getConnectionQueueInfo c cId + ServerQueueInfo {info = QueueInfo {qiSnd, qiNtf, qiSub, qiSize, qiMsg}} <- getConnectionQueueInfo c cId liftIO $ do qiSnd `shouldBe` qiSnd' qiNtf `shouldBe` False From 743676421db003036afdd183dd48fcf6f5e4c617 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 7 Jul 2024 21:17:12 +0100 Subject: [PATCH 114/125] ntf server: simplify and optimize subscriptions in server agent (#1219) * increase queue size * simplify * refactor to optimize memory usage and performance * comment * refactor * test delays --- src/Simplex/Messaging/Client/Agent.hs | 190 +++++++++--------- src/Simplex/Messaging/Notifications/Server.hs | 37 +--- src/Simplex/Messaging/Server.hs | 4 +- tests/AgentTests/FunctionalAPITests.hs | 1 + tests/AgentTests/NotificationTests.hs | 8 +- 5 files changed, 110 insertions(+), 130 deletions(-) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index e7c22eec2..99c77f67c 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -20,17 +20,15 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) -import Data.Bifunctor (bimap, first) +import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Either (partitionEithers) -import Data.List (partition) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (listToMaybe) import Data.Set (Set) +import qualified Data.Set as S import Data.Text.Encoding import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime) import Data.Tuple (swap) @@ -55,8 +53,8 @@ type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) (OwnServe data SMPClientAgentEvent = CAConnected SMPServer | CADisconnected SMPServer (Set SMPSub) - | CAResubscribed SMPServer (NonEmpty SMPSub) - | CASubError SMPServer (NonEmpty (SMPSub, SMPClientError)) + | CASubscribed SMPServer SMPSubParty (NonEmpty QueueId) + | CASubError SMPServer SMPSubParty (NonEmpty (QueueId, SMPClientError)) data SMPSubParty = SPRecipient | SPNotifier deriving (Eq, Ord, Show) @@ -86,9 +84,9 @@ defaultSMPClientAgentConfig = maxInterval = 10 * second }, persistErrorInterval = 30, -- seconds - msgQSize = 256, - agentQSize = 256, - agentSubsBatchSize = 900, + msgQSize = 1024, + agentQSize = 1024, + agentSubsBatchSize = 1360, ownServerDomains = [] } where @@ -192,7 +190,7 @@ getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worke isOwnServer :: SMPClientAgent -> SMPServer -> OwnServer isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} = let srv = strEncode $ L.head host - in any (\s -> s == srv || (B.cons '.' s) `B.isSuffixOf` srv) (ownServerDomains agentCfg) + in any (\s -> s == srv || B.cons '.' s `B.isSuffixOf` srv) (ownServerDomains agentCfg) -- | Run an SMP client for SMPClientVar connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient) @@ -212,15 +210,9 @@ connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, random where updateSubs sVar = do ss <- readTVar sVar - addPendingSubs sVar ss + addSubs_ (pendingSrvSubs ca) srv ss pure ss - addPendingSubs sVar ss = do - let ps = pendingSrvSubs ca - TM.lookup srv ps >>= \case - Just ss' -> TM.union ss ss' - _ -> TM.insert srv sVar ps - serverDown :: Map SMPSub C.APrivateAuthKey -> IO () serverDown ss = unless (M.null ss) $ do notify ca . CADisconnected srv $ M.keysSet ss @@ -244,11 +236,11 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s runSubWorker = withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> do pending <- atomically getPending - forM_ pending $ \cs -> whenM (readTVarIO active) $ do - void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv cs) + unless (null pending) $ whenM (readTVarIO active) $ do + void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv pending) loop ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg - getPending = mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca) + getPending = maybe (pure M.empty) readTVar =<< TM.lookup srv (pendingSrvSubs ca) cleanup :: SessionVar (Async ()) -> STM () cleanup v = do -- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar. @@ -258,32 +250,22 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s reconnectSMPClient :: SMPClientAgent -> SMPServer -> Map SMPSub C.APrivateAuthKey -> ExceptT SMPClientError IO () reconnectSMPClient ca@SMPClientAgent {agentCfg} srv cs = - withSMP ca srv $ \smp -> do - subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs - let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs' + withSMP ca srv $ \smp -> liftIO $ do + currSubs <- atomically $ maybe (pure M.empty) readTVar =<< TM.lookup srv (srvSubs ca) + let (nSubs, rSubs) = foldr (groupSub currSubs) ([], []) $ M.assocs cs subscribe_ smp SPNotifier nSubs subscribe_ smp SPRecipient rSubs where - isNotifier = \case - SPNotifier -> True - SPRecipient -> False - subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO () - subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg) + groupSub :: Map SMPSub C.APrivateAuthKey -> (SMPSub, C.APrivateAuthKey) -> ([(QueueId, C.APrivateAuthKey)], [(QueueId, C.APrivateAuthKey)]) -> ([(QueueId, C.APrivateAuthKey)], [(QueueId, C.APrivateAuthKey)]) + groupSub currSubs (s@(party, qId), k) (nSubs, rSubs) + | M.member s currSubs = (nSubs, rSubs) + | otherwise = case party of + SPNotifier -> (s' : nSubs, rSubs) + SPRecipient -> (nSubs, s' : rSubs) where - subscribeBatch subs' = do - let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs' - rs <- liftIO $ smpSubscribeQueues party ca smp srv subs'' - let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) = - L.zipWith (first . const) subs' rs - rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] = - map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs' - (errs, oks) = partitionEithers rs'' - (tempErrs, finalErrs) = partition (temporaryClientError . snd) errs - mapM_ (atomically . addSubscription ca srv) oks - mapM_ (notify ca . CAResubscribed srv) $ L.nonEmpty $ map fst oks - mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs - mapM_ (notify ca . CASubError srv) $ L.nonEmpty finalErrs - mapM_ (throwE . snd) $ listToMaybe tempErrs + s' = (qId, k) + subscribe_ :: SMPClient -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> IO () + subscribe_ smp party = mapM_ (smpSubscribeQueues party ca smp srv) . toChunks (agentSubsBatchSize agentCfg) notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m () notify ca evt = atomically $ writeTBQueue (agentQ ca) evt @@ -297,7 +279,8 @@ getConnectedSMPServerClient SMPClientAgent {smpClients} srv = $>>= \case (_, Right r) -> pure $ Just $ Right r (v, Left (e, ts_)) -> - pure ts_ $>>= \ts -> -- proxy will create a new connection if ts_ is Nothing + pure ts_ $>>= \ts -> + -- proxy will create a new connection if ts_ is Nothing ifM ((ts <) <$> liftIO getCurrentTime) -- error persistence interval period expired? (Nothing <$ atomically (removeSessVar v srv smpClients)) -- proxy will create a new connection @@ -334,86 +317,99 @@ withSMP ca srv action = (getSMPServerClient' ca srv >>= action) `catchE` logSMPE liftIO $ putStrLn $ "SMP error (" <> show srv <> "): " <> show e throwE e -subscribeQueue :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> ExceptT SMPClientError IO () -subscribeQueue ca srv sub = do - atomically $ addPendingSubscription ca srv sub - withSMP ca srv $ \smp -> subscribe_ smp `catchE` handleErr - where - subscribe_ smp = do - smpSubscribe smp sub - atomically $ addSubscription ca srv sub - - handleErr e = do - atomically . when (e /= PCENetworkError && e /= PCEResponseTimeout) $ - removePendingSubscription ca srv (fst sub) - throwE e - -subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateAuthKey) -> IO (NonEmpty (RecipientId, Either SMPClientError ())) +subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateAuthKey) -> IO () subscribeQueuesSMP = subscribeQueues_ SPRecipient -subscribeQueuesNtfs :: SMPClientAgent -> SMPServer -> NonEmpty (NotifierId, NtfPrivateAuthKey) -> IO (NonEmpty (NotifierId, Either SMPClientError ())) +subscribeQueuesNtfs :: SMPClientAgent -> SMPServer -> NonEmpty (NotifierId, NtfPrivateAuthKey) -> IO () subscribeQueuesNtfs = subscribeQueues_ SPNotifier -subscribeQueues_ :: SMPSubParty -> SMPClientAgent -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO (NonEmpty (QueueId, Either SMPClientError ())) +subscribeQueues_ :: SMPSubParty -> SMPClientAgent -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO () subscribeQueues_ party ca srv subs = do - atomically $ forM_ subs $ addPendingSubscription ca srv . first (party,) + atomically $ addPendingSubs ca srv party $ L.toList subs runExceptT (getSMPServerClient' ca srv) >>= \case - Left e -> pure $ L.map ((,Left e) . fst) subs Right smp -> smpSubscribeQueues party ca smp srv subs + Left _ -> pure () -- no call to reconnectClient - failing getSMPServerClient' does that -smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO (NonEmpty (QueueId, Either SMPClientError ())) +smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO () smpSubscribeQueues party ca smp srv subs = do - rs <- L.zip subs <$> subscribe smp (L.map swap subs) - atomically $ forM rs $ \(sub, r) -> - (fst sub,) <$> case r of - Right () -> do - addSubscription ca srv $ first (party,) sub - pure $ Right () - Left e -> do - when (e /= PCENetworkError && e /= PCEResponseTimeout) $ - removePendingSubscription ca srv (party, fst sub) - pure $ Left e + rs <- subscribe smp $ L.map swap subs + rs' <- + atomically $ + ifM + (activeClientSession ca smp srv) + (Just <$> processSubscriptions rs) + (pure Nothing) + case rs' of + Just (tempErrs, finalErrs, oks, _) -> do + notify_ CASubscribed $ map fst oks + notify_ CASubError finalErrs + when tempErrs $ reconnectClient ca srv + Nothing -> reconnectClient ca srv where + processSubscriptions :: NonEmpty (Either SMPClientError ()) -> STM (Bool, [(QueueId, SMPClientError)], [(QueueId, C.APrivateAuthKey)], [QueueId]) + processSubscriptions rs = do + pending <- maybe (pure M.empty) readTVar =<< TM.lookup srv (pendingSrvSubs ca) + let acc@(_, _, oks, notPending) = foldr (groupSub pending) (False, [], [], []) (L.zip subs rs) + unless (null oks) $ addSubscriptions ca srv party oks + unless (null notPending) $ removePendingSubs ca srv party notPending + pure acc + groupSub :: Map SMPSub C.APrivateAuthKey -> ((QueueId, C.APrivateAuthKey), Either SMPClientError ()) -> (Bool, [(QueueId, SMPClientError)], [(QueueId, C.APrivateAuthKey)], [QueueId]) -> (Bool, [(QueueId, SMPClientError)], [(QueueId, C.APrivateAuthKey)], [QueueId]) + groupSub pending (s@(qId, _), r) acc@(!tempErrs, finalErrs, oks, notPending) = case r of + Right () + | M.member (party, qId) pending -> (tempErrs, finalErrs, s : oks, qId : notPending) + | otherwise -> acc + Left e + | temporaryClientError e -> (True, finalErrs, oks, notPending) + | otherwise -> (tempErrs, (qId, e) : finalErrs, oks, qId : notPending) subscribe = case party of SPRecipient -> subscribeSMPQueues SPNotifier -> subscribeSMPQueuesNtfs + notify_ :: (SMPServer -> SMPSubParty -> NonEmpty a -> SMPClientAgentEvent) -> [a] -> IO () + notify_ evt qs = mapM_ (notify ca . evt srv party) $ L.nonEmpty qs + +activeClientSession :: SMPClientAgent -> SMPClient -> SMPServer -> STM Bool +activeClientSession ca smp srv = sameSess <$> tryReadSessVar srv (smpClients ca) + where + sessId = sessionId . thParams + sameSess = \case + Just (Right (_, smp')) -> sessId smp == sessId smp' + _ -> False showServer :: SMPServer -> ByteString showServer ProtocolServer {host, port} = strEncode host <> B.pack (if null port then "" else ':' : port) -smpSubscribe :: SMPClient -> (SMPSub, C.APrivateAuthKey) -> ExceptT SMPClientError IO () -smpSubscribe smp ((party, queueId), privKey) = subscribe_ smp privKey queueId +addSubscriptions :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> STM () +addSubscriptions = addSubsList_ . srvSubs +{-# INLINE addSubscriptions #-} + +addPendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> STM () +addPendingSubs = addSubsList_ . pendingSrvSubs +{-# INLINE addPendingSubs #-} + +addSubsList_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> STM () +addSubsList_ subs srv party ss = addSubs_ subs srv ss' where - subscribe_ = case party of - SPRecipient -> subscribeSMPQueue - SPNotifier -> subscribeSMPQueueNotifications + ss' = M.fromList $ map (first (party,)) ss -addSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM () -addSubscription ca srv sub = do - addSub_ (srvSubs ca) srv sub - removePendingSubscription ca srv $ fst sub - -addPendingSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM () -addPendingSubscription = addSub_ . pendingSrvSubs - -addSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM () -addSub_ subs srv (s, key) = +addSubs_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> Map SMPSub C.APrivateAuthKey -> STM () +addSubs_ subs srv ss = TM.lookup srv subs >>= \case - Just m -> TM.insert s key m - _ -> TM.singleton s key >>= \v -> TM.insert srv v subs + Just m -> TM.union ss m + _ -> newTVar ss >>= \v -> TM.insert srv v subs removeSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM () removeSubscription = removeSub_ . srvSubs - -removePendingSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM () -removePendingSubscription = removeSub_ . pendingSrvSubs +{-# INLINE removeSubscription #-} removeSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM () removeSub_ subs srv s = TM.lookup srv subs >>= mapM_ (TM.delete s) -getSubKey :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM (Maybe C.APrivateAuthKey) -getSubKey subs srv s = TM.lookup srv subs $>>= TM.lookup s +removePendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [QueueId] -> STM () +removePendingSubs = removeSubs_ . pendingSrvSubs +{-# INLINE removePendingSubs #-} -hasSub :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM Bool -hasSub subs srv s = maybe (pure False) (TM.member s) =<< TM.lookup srv subs +removeSubs_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSubParty -> [QueueId] -> STM () +removeSubs_ subs srv party qs = TM.lookup srv subs >>= mapM_ (`modifyTVar'` (`M.withoutKeys` ss)) + where + ss = S.fromList $ map (party,) qs diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 5d3b4d806..2bf8dbcbf 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -188,33 +188,16 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge runSMPSubscriber :: SMPSubscriber -> M () runSMPSubscriber SMPSubscriber {newSubQ = subscriberSubQ} = forever $ do - subs <- atomically (peekTQueue subscriberSubQ) + subs <- atomically $ readTQueue subscriberSubQ let subs' = L.map (\(NtfSub sub) -> sub) subs srv = server $ L.head subs logSubStatus srv "subscribing" $ length subs mapM_ (\NtfSubData {smpQueue} -> updateSubStatus smpQueue NSPending) subs' - rs <- liftIO $ subscribeQueues srv subs' - (subs'', oks, errs) <- foldM process ([], 0, []) rs - atomically $ do - void $ readTQueue subscriberSubQ - mapM_ (writeTQueue subscriberSubQ . L.map NtfSub) $ L.nonEmpty subs'' - logSubStatus srv "retrying" $ length subs'' - logSubStatus srv "subscribed" oks - logSubErrors srv errs - where - process :: ([NtfSubData], Int, [NtfSubStatus]) -> (NtfSubData, Either SMPClientError ()) -> M ([NtfSubData], Int, [NtfSubStatus]) - process (subs, oks, errs) (sub@NtfSubData {smpQueue}, r) = case r of - Right _ -> updateSubStatus smpQueue NSActive $> (subs, oks + 1, errs) - Left e -> update <$> handleSubError smpQueue e - where - update = \case - Just err -> (subs, oks, err : errs) -- permanent error, log and don't retry subscription - Nothing -> (sub : subs, oks, errs) -- temporary error, retry subscription + liftIO $ subscribeQueues srv subs' -- \| Subscribe to queues. The list of results can have a different order. - subscribeQueues :: SMPServer -> NonEmpty NtfSubData -> IO (NonEmpty (NtfSubData, Either SMPClientError ())) - subscribeQueues srv subs = - L.zipWith (\s r -> (s, snd r)) subs <$> subscribeQueuesNtfs ca srv (L.map sub subs) + subscribeQueues :: SMPServer -> NonEmpty NtfSubData -> IO () + subscribeQueues srv subs = subscribeQueuesNtfs ca srv (L.map sub subs) where sub NtfSubData {smpQueue = SMPQueueNtf {notifierId}, notifierKey} = (notifierId, notifierKey) @@ -239,7 +222,7 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge incNtfStat ntfReceived Right SMP.END -> updateSubStatus smpQueue NSEnd Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e - Right _ -> logError $ "SMP server unexpected response" + Right _ -> logError "SMP server unexpected response" Left e -> logError $ "SMP client error: " <> tshow e receiveAgent = @@ -252,11 +235,11 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge forM_ subs $ \(_, ntfId) -> do let smpQueue = SMPQueueNtf srv ntfId updateSubStatus smpQueue NSInactive - CAResubscribed srv subs -> do - forM_ subs $ \(_, ntfId) -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive - logSubStatus srv "resubscribed" $ length subs - CASubError srv errs -> - forM errs (\((_, ntfId), err) -> handleSubError (SMPQueueNtf srv ntfId) err) + CASubscribed srv _ subs -> do + forM_ subs $ \ntfId -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive + logSubStatus srv "subscribed" $ length subs + CASubError srv _ errs -> + forM errs (\(ntfId, err) -> handleSubError (SMPQueueNtf srv ntfId) err) >>= logSubErrors srv . catMaybes . L.toList logSubStatus srv event n = diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index e96e8b582..a85036978 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -193,8 +193,8 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do CAConnected srv -> logInfo $ "SMP server connected " <> showServer' srv CADisconnected srv [] -> logInfo $ "SMP server disconnected " <> showServer' srv CADisconnected srv subs -> logError $ "SMP server disconnected " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) - CAResubscribed srv subs -> logError $ "SMP server resubscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) - CASubError srv errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs) + CASubscribed srv _ subs -> logError $ "SMP server subscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CASubError srv _ errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs) where showServer' = decodeLatin1 . strEncode . host diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 9cf7c2793..71fc2c6f0 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -1038,6 +1038,7 @@ testIncreaseConnAgentVersionStartDifferentVersion t = do -- version increases to max compatible disposeAgentClient alice + threadDelay 250000 alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB runRight_ $ do diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index f5988672d..a104c6cf5 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -374,20 +374,20 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} baseId ali -- alice client already has subscription for the connection Left (CMD PROHIBITED _) <- runExceptT $ getNotificationMessage alice nonce message - threadDelay 300000 + threadDelay 500000 suspendAgent alice 0 closeSQLiteStore store - threadDelay 300000 + threadDelay 500000 -- aliceNtf client doesn't have subscription and is allowed to get notification message withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do (_, [SMPMsgMeta {msgFlags = MsgFlags True}]) <- getNotificationMessage aliceNtf nonce message pure () - threadDelay 300000 + threadDelay 500000 reopenSQLiteStore store foregroundAgent alice - threadDelay 300000 + threadDelay 500000 runRight_ $ do get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False From 6e76221e079d125925c261a3ed09b4b94d50d6a1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 8 Jul 2024 21:47:42 +0100 Subject: [PATCH 115/125] agent: fix possible dead lock between sending and receiving messages, stress test for message delivery (#1224) * agent: fix possible dead lock between sending and receiving messages, stress test for message delivery * deliver events after the lock is released * delayed delivery in command processing too * tests: increase message expiration time --- src/Simplex/Messaging/Agent.hs | 36 ++++++--- src/Simplex/Messaging/Server/Env/STM.hs | 1 - src/Simplex/Messaging/Server/Main.hs | 1 - tests/AgentTests/FunctionalAPITests.hs | 103 ++++++++++++++++++++++-- tests/SMPClient.hs | 1 - 5 files changed, 121 insertions(+), 21 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index c7b5f5390..2262a96e5 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1126,10 +1126,14 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do lift $ waitForWork doWork atomically $ throwWhenInactive c atomically $ beginAgentOperation c AOSndNetwork - withWork c doWork (`getPendingServerCommand` server_) $ processCmd (riFast ri) + withWork c doWork (`getPendingServerCommand` server_) $ runProcessCmd (riFast ri) where - processCmd :: RetryInterval -> PendingCommand -> AM () - processCmd ri PendingCommand {cmdId, corrId, userId, connId, command} = case command of + runProcessCmd ri cmd = do + pending <- newTVarIO [] + processCmd ri cmd pending + mapM_ (atomically . writeTBQueue subQ) . reverse =<< readTVarIO pending + processCmd :: RetryInterval -> PendingCommand -> TVar [ATransmission] -> AM () + processCmd ri PendingCommand {cmdId, corrId, userId, connId, command} pendingCmds = case command of AClientCommand cmd -> case cmd of NEW enableNtfs (ACM cMode) pqEnc subMode -> noServer $ do usedSrvs <- newTVarIO ([] :: [SMPServer]) @@ -1145,7 +1149,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do LET confId ownCInfo -> withServer' . tryCommand $ allowConnection' c connId confId ownCInfo >> notify OK ACK msgId rcptInfo_ -> withServer' . tryCommand $ ackMessage' c connId msgId rcptInfo_ >> notify OK SWCH -> - noServer . tryCommand . withConnLock c connId "switchConnection" $ + noServer . tryWithLock "switchConnection" $ withStore c (`getConn` connId) >>= \case SomeConn _ conn@(DuplexConnection _ (replaced :| _rqs) _) -> switchDuplexConnection c conn replaced >>= notify . SWITCH QDRcv SPStarted @@ -1247,7 +1251,9 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do internalErr s = cmdError $ INTERNAL $ s <> ": " <> show (agentCommandTag command) cmdError e = notify (ERR e) >> withStore' c (`deleteCommand` cmdId) notify :: forall e. AEntityI e => AEvent e -> AM () - notify cmd = atomically $ writeTBQueue subQ (corrId, connId, AEvt (sAEntity @e) cmd) + notify cmd = + let t = (corrId, connId, AEvt (sAEntity @e) cmd) + in atomically $ ifM (isFullTBQueue subQ) (modifyTVar' pendingCmds (t :)) (writeTBQueue subQ t) -- ^ ^ ^ async command processing / enqueueMessages :: AgentClient -> ConnData -> NonEmpty SndQueue -> MsgFlags -> AMessage -> AM (AgentMsgId, PQEncryption) @@ -2159,7 +2165,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), _v, sessId forM_ ts $ \(entId, t) -> case t of STEvent msgOrErr -> withRcvConn entId $ \rq@RcvQueue {connId} conn -> case msgOrErr of - Right msg -> processSMP rq conn (toConnData conn) msg + Right msg -> runProcessSMP rq conn (toConnData conn) msg Left e -> lift $ notifyErr connId e STResponse (Cmd SRecipient cmd) respOrErr -> withRcvConn entId $ \rq conn -> case cmd of @@ -2167,11 +2173,11 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), _v, sessId Right SMP.OK -> processSubOk rq upConnIds Right msg@SMP.MSG {} -> do processSubOk rq upConnIds -- the connection is UP even when processing this particular message fails - processSMP rq conn (toConnData conn) msg + runProcessSMP rq conn (toConnData conn) msg Right r -> processSubErr rq $ unexpectedResponse r Left e -> unless (temporaryClientError e) $ processSubErr rq e -- timeout/network was already reported SMP.ACK _ -> case respOrErr of - Right msg@SMP.MSG {} -> processSMP rq conn (toConnData conn) msg + Right msg@SMP.MSG {} -> runProcessSMP rq conn (toConnData conn) msg _ -> pure () -- TODO process OK response to ACK _ -> pure () -- TODO process expired response to DEL STResponse {} -> pure () -- TODO process expired responses to sent messages @@ -2209,12 +2215,18 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), _v, sessId notify' connId msg = atomically $ writeTBQueue subQ ("", connId, AEvt (sAEntity @e) msg) notifyErr :: ConnId -> SMPClientError -> AM' () notifyErr connId = notify' connId . ERR . protocolClientError SMP (B.unpack $ strEncode srv) - processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM () + runProcessSMP :: RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM () + runProcessSMP rq conn cData msg = do + pending <- newTVarIO [] + processSMP rq conn cData msg pending + mapM_ (atomically . writeTBQueue subQ) . reverse =<< readTVarIO pending + processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> TVar [ATransmission] -> AM () processSMP rq@RcvQueue {rcvId = rId, sndSecure, e2ePrivKey, e2eDhSecret, status} conn cData@ConnData {connId, connAgentVersion, ratchetSyncState = rss} - smpMsg = + smpMsg + pendingMsgs = withConnLock c connId "processSMP" $ case smpMsg of SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> do atomically $ incSMPServerStat c userId srv recvMsgs @@ -2395,7 +2407,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), _v, sessId r -> unexpected r where notify :: forall e m. (AEntityI e, MonadIO m) => AEvent e -> m () - notify = notify' connId + notify msg = + let t = ("", connId, AEvt (sAEntity @e) msg) + in atomically $ ifM (isFullTBQueue subQ) (modifyTVar' pendingMsgs (t :)) (writeTBQueue subQ t) prohibited :: Text -> AM () prohibited s = do diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 4217ea9b9..559aea280 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -47,7 +47,6 @@ data ServerConfig = ServerConfig { transports :: [(ServiceName, ATransport)], smpHandshakeTimeout :: Int, tbqSize :: Natural, - -- serverTbqSize :: Natural, msgQueueQuota :: Int, queueIdBytes :: Int, msgIdBytes :: Int, diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 7af57ba25..04e14544c 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -255,7 +255,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = { transports = iniTransports ini, smpHandshakeTimeout = 120000000, tbqSize = 64, - -- serverTbqSize = 1024, msgQueueQuota = 128, queueIdBytes = 24, msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20 diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 71fc2c6f0..00a6e1c55 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -263,6 +263,11 @@ functionalAPITests t = do withSmpServer t testAgentClient3 it "should establish connection without PQ encryption and enable it" $ withSmpServer t testEnablePQEncryption + describe "Duplex connection - delivery stress test" $ do + describe "one way (50)" $ testMatrix2Stress t $ runAgentClientStressTestOneWay 50 + xdescribe "one way (1000)" $ testMatrix2Stress t $ runAgentClientStressTestOneWay 1000 + describe "two way concurrently (50)" $ testMatrix2Stress t $ runAgentClientStressTestConc 25 + xdescribe "two way concurrently (1000)" $ testMatrix2Stress t $ runAgentClientStressTestConc 500 describe "Establishing duplex connection, different PQ settings" $ do testPQMatrix2 t $ runAgentClientTestPQ True describe "Establishing duplex connection v2, different Ratchet versions" $ @@ -482,6 +487,19 @@ testMatrix2 t runTest = do it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False +testMatrix2Stress :: HasCallStack => ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testMatrix2Stress t runTest = do + it "current, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 aCfg aCfg (initAgentServersProxy SPMAlways SPFProhibit) 1 $ runTest PQSupportOn True + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 aProxyCfgV8 aProxyCfgV8 (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True + it "current" $ withSmpServer t $ runTestCfg2 aCfg aCfg 1 $ runTest PQSupportOn False + it "prev" $ withSmpServer t $ runTestCfg2 aCfgVPrev aCfgVPrev 3 $ runTest PQSupportOff False + it "prev to current" $ withSmpServer t $ runTestCfg2 aCfgVPrev aCfg 3 $ runTest PQSupportOff False + it "current to prev" $ withSmpServer t $ runTestCfg2 aCfg aCfgVPrev 3 $ runTest PQSupportOff False + where + aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval} + aProxyCfgV8 = agentProxyCfgV8 {messageRetryInterval = fastMessageRetryInterval} + aCfgVPrev = agentCfgVPrev {messageRetryInterval = fastMessageRetryInterval} + testBasicMatrix2 :: HasCallStack => ATransport -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testBasicMatrix2 t runTest = do it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 1 $ runTest @@ -616,6 +634,71 @@ runAgentClientTestPQ viaProxy (alice, aPQ) (bob, bPQ) baseId = pqConnectionMode :: InitialKeys -> PQSupport -> Bool pqConnectionMode pqMode1 pqMode2 = supportPQ (CR.connPQEncryption pqMode1) && supportPQ pqMode2 +runAgentClientStressTestOneWay :: HasCallStack => Int64 -> PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientStressTestOneWay n pqSupport viaProxy alice bob baseId = runRight_ $ do + let pqEnc = PQEncryption $ supportPQ pqSupport + (aliceId, bobId) <- makeConnection_ pqSupport alice bob + let proxySrv = if viaProxy then Just testSMPServer else Nothing + message i = "message " <> bshow i + concurrently_ + ( forM_ ([1 .. n] :: [Int64]) $ \i -> do + mId <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags (message i) + liftIO $ do + mId >= i `shouldBe` True + let getEvent = + get alice >>= \case + ("", c, A.SENT mId' srv) -> c == bobId && mId' >= baseId + i && srv == proxySrv `shouldBe` True + ("", c, QCONT) -> do + c == bobId `shouldBe` True + getEvent + r -> expectationFailure $ "wrong message: " <> show r + getEvent + ) + ( forM_ ([1 .. n] :: [Int64]) $ \i -> do + get bob >>= \case + ("", c, Msg' mId pq msg) -> do + liftIO $ c == aliceId && mId >= baseId + i && pq == pqEnc && msg == message i `shouldBe` True + ackMessage bob aliceId mId Nothing + r -> liftIO $ expectationFailure $ "wrong message: " <> show r + ) + liftIO $ noMessagesIngoreQCONT alice "nothing else should be delivered to alice" + liftIO $ noMessagesIngoreQCONT bob "nothing else should be delivered to bob" + where + msgId = subtract baseId . fst + +runAgentClientStressTestConc :: HasCallStack => Int64 -> PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientStressTestConc n pqSupport viaProxy alice bob baseId = runRight_ $ do + let pqEnc = PQEncryption $ supportPQ pqSupport + (aliceId, bobId) <- makeConnection_ pqSupport alice bob + let proxySrv = if viaProxy then Just testSMPServer else Nothing + message i = "message " <> bshow i + loop a bId mIdVar i = do + when (i <= n) $ do + mId <- msgId <$> A.sendMessage a bId pqEnc SMP.noMsgFlags (message i) + liftIO $ mId >= i `shouldBe` True + let getEvent = do + get a >>= \case + ("", c, A.SENT _ srv) -> liftIO $ c == bId && srv == proxySrv `shouldBe` True + ("", c, QCONT) -> do + liftIO $ c == bId `shouldBe` True + getEvent + ("", c, Msg' mId pq msg) -> do + -- tests that mId increases + liftIO $ (mId >) <$> atomically (swapTVar mIdVar mId) `shouldReturn` True + liftIO $ c == bId && pq == pqEnc && ("message " `B.isPrefixOf` msg) `shouldBe` True + ackMessage a bId mId Nothing + r -> liftIO $ expectationFailure $ "wrong message: " <> show r + getEvent + amId <- newTVarIO 0 + bmId <- newTVarIO 0 + concurrently_ + (forM_ ([1 .. n * 2] :: [Int64]) $ loop alice bobId amId) + (forM_ ([1 .. n * 2] :: [Int64]) $ loop bob aliceId bmId) + liftIO $ noMessagesIngoreQCONT alice "nothing else should be delivered to alice" + liftIO $ noMessagesIngoreQCONT bob "nothing else should be delivered to bob" + where + msgId = subtract baseId . fst + testEnablePQEncryption :: HasCallStack => IO () testEnablePQEncryption = withAgentClients2 $ \ca cb -> runRight_ $ do @@ -789,10 +872,17 @@ runAgentClientContactTestPQ3 viaProxy (alice, aPQ) (bob, bPQ) (tom, tPQ) baseId ackMessage a bId (baseId + 2) Nothing noMessages :: HasCallStack => AgentClient -> String -> Expectation -noMessages c err = tryGet `shouldReturn` () +noMessages = noMessages_ False + +noMessagesIngoreQCONT :: AgentClient -> String -> Expectation +noMessagesIngoreQCONT = noMessages_ True + +noMessages_ :: Bool -> HasCallStack => AgentClient -> String -> Expectation +noMessages_ ingoreQCONT c err = tryGet `shouldReturn` () where tryGet = 10000 `timeout` get c >>= \case + Just (_, _, QCONT) | ingoreQCONT -> noMessages_ ingoreQCONT c err Just msg -> error $ err <> ": " <> show msg _ -> return () @@ -1194,7 +1284,6 @@ testDeliveryAfterSubscriptionError t = do withAgentClients2 $ \a b -> do Left (BROKER _ NETWORK) <- runExceptT $ subscribeConnection a bId Left (BROKER _ NETWORK) <- runExceptT $ subscribeConnection b aId - pure () withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do withUP a bId $ \case ("", c, SENT 2) -> c == bId; _ -> False withUP b aId $ \case ("", c, Msg "hello") -> c == aId; _ -> False @@ -1230,13 +1319,13 @@ testMsgDeliveryQuotaExceeded t = testExpireMessage :: HasCallStack => ATransport -> IO () testExpireMessage t = - withAgent 1 agentCfg {messageTimeout = 1, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> + withAgent 1 agentCfg {messageTimeout = 1.5, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> withAgent 2 agentCfg initAgentServers testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ makeConnection a b nGet a =##> \case ("", "", DOWN _ [c]) -> c == bId; _ -> False nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False 2 <- runRight $ sendMessage a bId SMP.noMsgFlags "1" - threadDelay 1000000 + threadDelay 1500000 3 <- runRight $ sendMessage a bId SMP.noMsgFlags "2" -- this won't expire get a =##> \case ("", c, MERR 2 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False withSmpServerStoreLogOn t testPort $ \_ -> runRight_ $ do @@ -1246,7 +1335,7 @@ testExpireMessage t = testExpireManyMessages :: HasCallStack => ATransport -> IO () testExpireManyMessages t = - withAgent 1 agentCfg {messageTimeout = 1, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> + withAgent 1 agentCfg {messageTimeout = 1.5, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> withAgent 2 agentCfg initAgentServers testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ makeConnection a b runRight_ $ do @@ -1255,7 +1344,7 @@ testExpireManyMessages t = 2 <- sendMessage a bId SMP.noMsgFlags "1" 3 <- sendMessage a bId SMP.noMsgFlags "2" 4 <- sendMessage a bId SMP.noMsgFlags "3" - liftIO $ threadDelay 1000000 + liftIO $ threadDelay 1500000 5 <- sendMessage a bId SMP.noMsgFlags "4" -- this won't expire get a =##> \case ("", c, MERR 2 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False -- get a =##> \case ("", c, MERRS [5, 6] (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False @@ -1275,7 +1364,7 @@ testExpireManyMessages t = withUP b aId $ \case ("", _, MsgErr 2 (MsgSkipped 2 4) "4") -> True; _ -> False ackMessage b aId 2 Nothing -withUP :: AgentClient -> ConnId -> (AEntityTransmission 'AEConn -> Bool) -> ExceptT AgentErrorType IO () +withUP :: HasCallStack => AgentClient -> ConnId -> (AEntityTransmission 'AEConn -> Bool) -> ExceptT AgentErrorType IO () withUP a bId p = liftIO $ getInAnyOrder diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 144ad8b10..736016b3b 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -100,7 +100,6 @@ cfg = { transports = [], smpHandshakeTimeout = 60000000, tbqSize = 1, - -- serverTbqSize = 1, msgQueueQuota = 4, queueIdBytes = 24, msgIdBytes = 24, From 21abc5cabe371e0b111f04984689457269ff10f1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 8 Jul 2024 23:12:01 +0100 Subject: [PATCH 116/125] smp server: reduce the number of threads by delivering message to subscription when it is sent (#1222) * smp server: reduce the number of threads by delivering message to subscription when it is sent * test delay * test delay --- src/Simplex/Messaging/Server.hs | 92 +++++++++++++------- src/Simplex/Messaging/Server/MsgStore/STM.hs | 4 +- tests/AgentTests/FunctionalAPITests.hs | 3 +- tests/ServerTests.hs | 20 +++-- 4 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index a85036978..567ff13b6 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -683,7 +683,7 @@ forkClient Client {endThreads, endThreadSeq} label action = do mkWeakThreadId t >>= atomically . modifyTVar' endThreads . IM.insert tId client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M () -client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId, procThreads} Server {subscribedQ, ntfSubscribedQ, notifiers} = do +client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId, procThreads} Server {subscribedQ, ntfSubscribedQ, subscribers, notifiers} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands" forever $ atomically (readTBQueue rcvQ) @@ -921,7 +921,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi deliver sub = do q <- getStoreMsgQueue "SUB" rId msg_ <- atomically $ tryPeekMsg q - deliverMessage "SUB" qr rId sub q msg_ + deliverMessage "SUB" qr rId sub msg_ getMessage :: QueueRec -> M (Transmission BrokerMsg) getMessage qr = time "GET" $ do @@ -978,7 +978,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi _ -> do (deletedMsg_, msg_) <- atomically $ tryDelPeekMsg q msgId mapM_ updateStats deletedMsg_ - deliverMessage "ACK" qr queueId sub q msg_ + deliverMessage "ACK" qr queueId sub msg_ _ -> pure $ err NO_MSG where getDelivered :: TVar Sub -> STM (Maybe Sub) @@ -1024,7 +1024,8 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Nothing -> do atomically $ modifyTVar' (msgSentQuota stats) (+ 1) pure $ err QUOTA - Just msg -> time "SEND ok" $ do + Just (msg, wasEmpty) -> time "SEND ok" $ do + when wasEmpty $ tryDeliverMessage msg when (notification msgFlags) $ do forM_ (notifier qr) $ \ntf -> do asks random >>= atomically . trySendNotification ntf msg >>= \case @@ -1058,6 +1059,53 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi stats <- asks serverStats atomically $ modifyTVar' (msgExpired stats) (+ deleted) + -- The condition for delivery of the message is: + -- - the queue was empty when the message was sent, + -- - there is subscribed recipient, + -- - no message was "delivered" that was not acknowledged. + -- If the send queue of the subscribed client is not full the message is put there in the same transaction. + -- If the queue is not full, then the thread is created where these checks are made: + -- - it is the same subscribed client (in case it was reconnected it would receive message via SUB command) + -- - nothing was delivered to this subscription (to avoid race conditions with the recipient). + tryDeliverMessage :: Message -> M () + tryDeliverMessage msg = atomically deliverToSub >>= mapM_ forkDeliver + where + rId = recipientId qr + deliverToSub = + TM.lookup rId subscribers + $>>= \rc@Client {subscriptions = subs, sndQ = q} -> TM.lookup rId subs + $>>= \sub -> readTVar sub >>= \case + s@Sub {subThread = NoSub, delivered} -> + tryTakeTMVar delivered >>= \case + Just _ -> pure Nothing -- if a message was already delivered, should not deliver more + Nothing -> + ifM + (isFullTBQueue q) + (modifyTVar' sub (\s' -> s' {subThread = SubPending}) $> Just (rc, sub)) + (deliver q s $> Nothing) + _ -> pure Nothing + deliver q s = do + let encMsg = encryptMsg qr msg + writeTBQueue q [(CorrId "", rId, MSG encMsg)] + void $ setDelivered s msg + forkDeliver (rc@Client {sndQ = q}, sub) = do + t <- mkWeakThreadId =<< forkIO deliverThread + atomically . modifyTVar' sub $ \case + -- this case is needed because deliverThread can exit before it + s@Sub {subThread = SubPending} -> s {subThread = SubThread t} + s -> s + where + deliverThread = do + labelMyThread $ B.unpack ("client $" <> encode sessionId) <> " deliver/SEND" + time "deliver" . atomically $ + whenM (maybe False (sameClientId rc) <$> TM.lookup rId subscribers) $ do + s@Sub {delivered} <- readTVar sub + tryTakeTMVar delivered >>= \case + Just _ -> pure () -- if a message was already delivered, should not deliver more + Nothing -> do + deliver q s + writeTVar sub $! s {subThread = NoSub} + trySendNotification :: NtfCreds -> Message -> TVar ChaChaDRG -> STM (Maybe Bool) trySendNotification NtfCreds {notifierId, rcvNtfDhSecret} msg ntfNonceDrg = mapM (writeNtf notifierId msg rcvNtfDhSecret ntfNonceDrg) =<< TM.lookup notifierId notifiers @@ -1132,35 +1180,17 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi verified = \case VRVerified qr -> Right (qr, (corrId', entId', cmd')) VRFailed -> Left (corrId', entId', ERR AUTH) - deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> M (Transmission BrokerMsg) - deliverMessage name qr rId sub q msg_ = time (name <> " deliver") $ do - readTVarIO sub >>= \case - s@Sub {subThread = NoSub} -> - case msg_ of - Just msg -> - let encMsg = encryptMsg qr msg - in atomically (setDelivered s msg) $> (corrId, rId, MSG encMsg) - _ -> forkSub $> resp - _ -> pure resp + deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> Maybe Message -> M (Transmission BrokerMsg) + deliverMessage name qr rId sub msg_ = time (name <> " deliver") . atomically $ + readTVar sub >>= \case + Sub {subThread = ProhibitSub} -> pure resp + s -> case msg_ of + Just msg -> + let encMsg = encryptMsg qr msg + in setDelivered s msg $> (corrId, rId, MSG encMsg) + _ -> pure resp where resp = (corrId, rId, OK) - forkSub :: M () - forkSub = do - atomically . modifyTVar' sub $ \s -> s {subThread = SubPending} - t <- mkWeakThreadId =<< forkIO subscriber - atomically . modifyTVar' sub $ \case - s@Sub {subThread = SubPending} -> s {subThread = SubThread t} - s -> s - where - subscriber = do - labelMyThread $ B.unpack ("client $" <> encode sessionId) <> " subscriber/" <> T.unpack name - msg <- atomically $ peekMsg q - time "subscriber" . atomically $ do - let encMsg = encryptMsg qr msg - writeTBQueue sndQ [(CorrId "", rId, MSG encMsg)] - s <- readTVar sub - void $ setDelivered s msg - writeTVar sub $! s {subThread = NoSub} time :: T.Text -> M a -> M a time name = timed name queueId diff --git a/src/Simplex/Messaging/Server/MsgStore/STM.hs b/src/Simplex/Messaging/Server/MsgStore/STM.hs index e315c4fe5..c8f78e2fb 100644 --- a/src/Simplex/Messaging/Server/MsgStore/STM.hs +++ b/src/Simplex/Messaging/Server/MsgStore/STM.hs @@ -75,7 +75,7 @@ snapshotMsgQueue st rId = TM.lookup rId st >>= maybe (pure []) (snapshotTQueue . mapM_ (writeTQueue q) msgs pure msgs -writeMsg :: MsgQueue -> Message -> STM (Maybe Message) +writeMsg :: MsgQueue -> Message -> STM (Maybe (Message, Bool)) writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = do canWrt <- readTVar canWrite empty <- isEmptyTQueue q @@ -85,7 +85,7 @@ writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = do writeTVar canWrite $! canWrt' modifyTVar' size (+ 1) if canWrt' - then writeTQueue q msg $> Just msg + then writeTQueue q msg $> Just (msg, empty) else (writeTQueue q $! msgQuota) $> Nothing else pure Nothing where diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 00a6e1c55..ab78d2ee9 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -2991,6 +2991,7 @@ testServerMultipleIdentities = bob' <- liftIO $ do Left (BROKER _ NETWORK) <- runExceptT $ joinConnection bob 1 True secondIdentityCReq "bob's connInfo" SMSubscribe disposeAgentClient bob + threadDelay 250000 getSMPAgentClient' 3 agentCfg initAgentServers testDB2 subscribeConnection bob' aliceId exchangeGreetingsMsgId 4 alice bobId bob' aliceId @@ -3140,7 +3141,7 @@ testServerQueueInfo = do pure () where checkEmptyQ c cId qiSnd' = do - r <- checkQ c cId qiSnd' (Just QSubThread) 0 Nothing + r <- checkQ c cId qiSnd' (Just QNoSub) 0 Nothing liftIO $ r `shouldBe` Nothing checkMsgQ c cId qiSize' = do r <- checkQ c cId True (Just QNoSub) qiSize' (Just MTMessage) diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 1fa76dfaa..10516b9f2 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -509,19 +509,21 @@ testWithStoreLog at@(ATransport t) = writeTVar senderId1 sId1 writeTVar notifierId nId Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, NSUB) - signSendRecv h sKey1 ("bcda", sId1, _SEND' "hello") >>= \case - Resp "bcda" _ OK -> pure () - r -> unexpected r - Resp "" _ (Msg mId1 msg1) <- tGet1 h + (mId1, msg1) <- + signSendRecv h sKey1 ("bcda", sId1, _SEND' "hello") >>= \case + Resp "" _ (Msg mId1 msg1) -> pure (mId1, msg1) + r -> error $ "unexpected response " <> take 100 (show r) + Resp "bcda" _ OK <- tGet1 h (decryptMsgV3 dhShared mId1 msg1, Right "hello") #== "delivered from queue 1" Resp "" _ (NMSG _ _) <- tGet1 h1 (sId2, rId2, rKey2, dhShared2) <- createAndSecureQueue h sPub2 atomically $ writeTVar senderId2 sId2 - signSendRecv h sKey2 ("cdab", sId2, _SEND "hello too") >>= \case - Resp "cdab" _ OK -> pure () - r -> unexpected r - Resp "" _ (Msg mId2 msg2) <- tGet1 h + (mId2, msg2) <- + signSendRecv h sKey2 ("cdab", sId2, _SEND "hello too") >>= \case + Resp "" _ (Msg mId2 msg2) -> pure (mId2, msg2) + r -> error $ "unexpected response " <> take 100 (show r) + Resp "cdab" _ OK <- tGet1 h (decryptMsgV3 dhShared2 mId2 msg2, Right "hello too") #== "delivered from queue 2" Resp "dabc" _ OK <- signSendRecv h rKey2 ("dabc", rId2, DEL) @@ -884,7 +886,7 @@ testMsgExpireOnInterval t = testSMPClient @c $ \sh -> do (sId, rId, rKey, _) <- testSMPClient @c $ \rh -> createAndSecureQueue rh sPub Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello (should expire)") - threadDelay 2500000 + threadDelay 3000000 testSMPClient @c $ \rh -> do signSendRecv rh rKey ("2", rId, SUB) >>= \case Resp "2" _ OK -> pure () From 26979ff6b59a157e8d6432909d05c9cbf208b015 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 9 Jul 2024 08:36:03 +0100 Subject: [PATCH 117/125] smp server: simplify client subscriptions (#1223) --- src/Simplex/Messaging/Server.hs | 81 ++++++++++++------------- src/Simplex/Messaging/Server/Env/STM.hs | 7 ++- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 567ff13b6..014bcd366 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -514,11 +514,11 @@ clientDisconnected c@Client {clientId, subscriptions, connected, sessionId, endT sameClientId :: Client -> Client -> Bool sameClientId Client {clientId} Client {clientId = cId'} = clientId == cId' -cancelSub :: TVar Sub -> IO () -cancelSub sub = - readTVarIO sub >>= \case - Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread - _ -> return () +cancelSub :: Sub -> IO () +cancelSub s = + readTVarIO (subThread s) >>= \case + SubThread t -> liftIO $ deRefWeak t >>= mapM_ killThread + _ -> pure () receive :: Transport c => THandleSMP c 'TServer -> Client -> M () receive h@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do @@ -901,23 +901,23 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Nothing -> do atomically $ modifyTVar' (qSub stats) (+ 1) newSub >>= deliver - Just sub -> - readTVarIO sub >>= \case - Sub {subThread = ProhibitSub} -> do + Just s@Sub {subThread} -> + readTVarIO subThread >>= \case + ProhibitSub -> do -- cannot use SUB in the same connection where GET was used atomically $ modifyTVar' (qSubProhibited stats) (+ 1) pure (corrId, rId, ERR $ CMD PROHIBITED) - s -> do + _ -> do atomically $ modifyTVar' (qSubDuplicate stats) (+ 1) - atomically (tryTakeTMVar $ delivered s) >> deliver sub + atomically (tryTakeTMVar $ delivered s) >> deliver s where - newSub :: M (TVar Sub) + newSub :: M Sub newSub = time "SUB newSub" . atomically $ do writeTQueue subscribedQ (rId, clnt) - sub <- newTVar =<< newSubscription NoSub + sub <- newSubscription NoSub TM.insert rId sub subscriptions pure sub - deliver :: TVar Sub -> M (Transmission BrokerMsg) + deliver :: Sub -> M (Transmission BrokerMsg) deliver sub = do q <- getStoreMsgQueue "SUB" rId msg_ <- atomically $ tryPeekMsg q @@ -928,9 +928,9 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi atomically (TM.lookup queueId subscriptions) >>= \case Nothing -> atomically newSub >>= getMessage_ - Just sub -> - readTVarIO sub >>= \case - s@Sub {subThread = ProhibitSub} -> + Just s@Sub {subThread} -> + readTVarIO subThread >>= \case + ProhibitSub -> atomically (tryTakeTMVar $ delivered s) >> getMessage_ s -- cannot use GET in the same connection where there is an active subscription @@ -939,8 +939,7 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi newSub :: STM Sub newSub = do s <- newSubscription ProhibitSub - sub <- newTVar s - TM.insert queueId sub subscriptions + TM.insert queueId s subscriptions pure s getMessage_ :: Sub -> M (Transmission BrokerMsg) getMessage_ s = do @@ -968,10 +967,10 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi Nothing -> pure $ err NO_MSG Just sub -> atomically (getDelivered sub) >>= \case - Just s -> do + Just st -> do q <- getStoreMsgQueue "ACK" queueId - case s of - Sub {subThread = ProhibitSub} -> do + case st of + ProhibitSub -> do deletedMsg_ <- atomically $ tryDelMsg q msgId mapM_ updateStats deletedMsg_ pure ok @@ -981,12 +980,11 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi deliverMessage "ACK" qr queueId sub msg_ _ -> pure $ err NO_MSG where - getDelivered :: TVar Sub -> STM (Maybe Sub) - getDelivered sub = do - s@Sub {delivered} <- readTVar sub + getDelivered :: Sub -> STM (Maybe SubscriptionThread) + getDelivered Sub {delivered, subThread} = do tryTakeTMVar delivered $>>= \msgId' -> if msgId == msgId' || B.null msgId - then pure $ Just s + then Just <$> readTVar subThread else putTMVar delivered msgId' $> Nothing updateStats :: Message -> M () updateStats = \case @@ -1074,37 +1072,36 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi deliverToSub = TM.lookup rId subscribers $>>= \rc@Client {subscriptions = subs, sndQ = q} -> TM.lookup rId subs - $>>= \sub -> readTVar sub >>= \case - s@Sub {subThread = NoSub, delivered} -> + $>>= \s@Sub {subThread, delivered} -> readTVar subThread >>= \case + NoSub -> tryTakeTMVar delivered >>= \case Just _ -> pure Nothing -- if a message was already delivered, should not deliver more Nothing -> ifM (isFullTBQueue q) - (modifyTVar' sub (\s' -> s' {subThread = SubPending}) $> Just (rc, sub)) + (writeTVar subThread SubPending $> Just (rc, s)) (deliver q s $> Nothing) _ -> pure Nothing deliver q s = do let encMsg = encryptMsg qr msg writeTBQueue q [(CorrId "", rId, MSG encMsg)] void $ setDelivered s msg - forkDeliver (rc@Client {sndQ = q}, sub) = do + forkDeliver (rc@Client {sndQ = q}, s@Sub {subThread, delivered}) = do t <- mkWeakThreadId =<< forkIO deliverThread - atomically . modifyTVar' sub $ \case + atomically . modifyTVar' subThread $ \case -- this case is needed because deliverThread can exit before it - s@Sub {subThread = SubPending} -> s {subThread = SubThread t} - s -> s + SubPending -> SubThread t + st -> st where deliverThread = do labelMyThread $ B.unpack ("client $" <> encode sessionId) <> " deliver/SEND" time "deliver" . atomically $ whenM (maybe False (sameClientId rc) <$> TM.lookup rId subscribers) $ do - s@Sub {delivered} <- readTVar sub tryTakeTMVar delivered >>= \case Just _ -> pure () -- if a message was already delivered, should not deliver more Nothing -> do deliver q s - writeTVar sub $! s {subThread = NoSub} + writeTVar subThread NoSub trySendNotification :: NtfCreds -> Message -> TVar ChaChaDRG -> STM (Maybe Bool) trySendNotification NtfCreds {notifierId, rcvNtfDhSecret} msg ntfNonceDrg = @@ -1180,11 +1177,11 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi verified = \case VRVerified qr -> Right (qr, (corrId', entId', cmd')) VRFailed -> Left (corrId', entId', ERR AUTH) - deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> Maybe Message -> M (Transmission BrokerMsg) - deliverMessage name qr rId sub msg_ = time (name <> " deliver") . atomically $ - readTVar sub >>= \case - Sub {subThread = ProhibitSub} -> pure resp - s -> case msg_ of + deliverMessage :: T.Text -> QueueRec -> RecipientId -> Sub -> Maybe Message -> M (Transmission BrokerMsg) + deliverMessage name qr rId s@Sub {subThread} msg_ = time (name <> " deliver") . atomically $ + readTVar subThread >>= \case + ProhibitSub -> pure resp + _ -> case msg_ of Just msg -> let encMsg = encryptMsg qr msg in setDelivered s msg $> (corrId, rId, MSG encMsg) @@ -1232,9 +1229,9 @@ client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessi pure QueueInfo {qiSnd = isJust senderKey, qiNtf = isJust notifier, qiSub, qiSize, qiMsg} pure (corrId, queueId, INFO info) where - mkQSub sub = do - Sub {subThread, delivered} <- readTVar sub - let qSubThread = case subThread of + mkQSub Sub {subThread, delivered} = do + st <- readTVar subThread + let qSubThread = case st of NoSub -> QNoSub SubPending -> QSubPending SubThread _ -> QSubThread diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 559aea280..b40e9fc16 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -144,7 +144,7 @@ type ClientId = Int data Client = Client { clientId :: ClientId, - subscriptions :: TMap RecipientId (TVar Sub), + subscriptions :: TMap RecipientId Sub, ntfSubscriptions :: TMap NotifierId (), rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)), sndQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), @@ -163,7 +163,7 @@ data Client = Client data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub data Sub = Sub - { subThread :: SubscriptionThread, + { subThread :: TVar SubscriptionThread, delivered :: TMVar MsgId } @@ -193,8 +193,9 @@ newClient nextClientId qSize thVersion sessionId createdAt = do return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, procThreads, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} newSubscription :: SubscriptionThread -> STM Sub -newSubscription subThread = do +newSubscription st = do delivered <- newEmptyTMVar + subThread <- newTVar st return Sub {subThread, delivered} newEnv :: ServerConfig -> IO Env From 017469b2de65c7e3ef1d680c2da466b320d1b061 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 9 Jul 2024 13:56:02 +0100 Subject: [PATCH 118/125] 6.0.0.0 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 94c2b6db0..fbf95a7e5 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.8.2.0 +version: 6.0.0.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index aec362196..b7b1ca76c 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.8.2.0 +version: 6.0.0.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and From ff2b00a0299343e39bd8d56fc43d97f7a8fe214c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:27:04 +0400 Subject: [PATCH 119/125] agent: change ProxyClientError json encoding (#1226) --- src/Simplex/Messaging/Client.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index e20b00039..1837a256b 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -130,7 +130,7 @@ import Numeric.Natural import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) import Simplex.Messaging.Protocol import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.TMap (TMap) @@ -847,11 +847,11 @@ data ProxiedRelay = ProxiedRelay data ProxyClientError = -- | protocol error response from proxy - ProxyProtocolError ErrorType + ProxyProtocolError {protocolErr :: ErrorType} | -- | unexpexted response - ProxyUnexpectedResponse String + ProxyUnexpectedResponse {responseStr :: String} | -- | error between proxy and server - ProxyResponseError ErrorType + ProxyResponseError {responseErr :: ErrorType} deriving (Eq, Show, Exception) instance StrEncoding ProxyClientError where @@ -1139,6 +1139,6 @@ $(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback) $(J.deriveJSON defaultJSON ''NetworkConfig) -$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) +$(J.deriveJSON (sumTypeJSON $ dropPrefix "Proxy") ''ProxyClientError) $(J.deriveJSON defaultJSON ''TBQueueInfo) From e56bd0b47b22781f9b5936e3b8a9d51cc3f9e8d5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 12 Jul 2024 12:41:55 +0100 Subject: [PATCH 120/125] agent: add known servers (#1225) * agent: add known servers * test delays * ServerCfg * json encoding * enabledServerCfg * checkUserServers --- src/Simplex/Messaging/Agent.hs | 35 ++++++++++----- src/Simplex/Messaging/Agent/Client.hs | 17 ++++---- src/Simplex/Messaging/Agent/Env/SQLite.hs | 52 +++++++++++++++++++++-- tests/AgentTests/FunctionalAPITests.hs | 24 +++++------ tests/SMPAgentClient.hs | 19 ++++++--- tests/SMPProxyTests.hs | 8 ++-- 6 files changed, 111 insertions(+), 44 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 2262a96e5..c08e04298 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -77,6 +77,7 @@ module Simplex.Messaging.Agent getConnectionServers, getConnectionRatchetAdHash, setProtocolServers, + checkUserServers, testProtocolServer, setNtfServers, setNetworkConfig, @@ -172,7 +173,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..)) import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) -import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth, sndAuthKeySMPClientVersion) +import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolType (..), ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, sndAuthKeySMPClientVersion) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM @@ -197,15 +198,18 @@ getSMPAgentClient = getSMPAgentClient_ 1 {-# INLINE getSMPAgentClient #-} getSMPAgentClient_ :: Int -> AgentConfig -> InitialAgentServers -> SQLiteStore -> Bool -> IO AgentClient -getSMPAgentClient_ clientId cfg initServers store backgroundMode = - liftIO $ newSMPAgentEnv cfg store >>= runReaderT runAgent +getSMPAgentClient_ clientId cfg initServers@InitialAgentServers {smp, xftp} store backgroundMode = + newSMPAgentEnv cfg store >>= runReaderT runAgent where runAgent = do + liftIO $ checkServers "SMP" smp >> checkServers "XFTP" xftp currentTs <- liftIO getCurrentTime c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers currentTs =<< ask t <- runAgentThreads c `forkFinally` const (liftIO $ disconnectAgentClient c) atomically . writeTVar acThread . Just =<< mkWeakThreadId t pure c + checkServers protocol srvs = + forM_ (M.assocs srvs) $ \(userId, srvs') -> checkUserServers ("getSMPAgentClient " <> protocol <> " " <> tshow userId) srvs' runAgentThreads c | backgroundMode = run c "subscriber" $ subscriber c | otherwise = do @@ -271,7 +275,7 @@ resumeAgentClient :: AgentClient -> IO () resumeAgentClient c = atomically $ writeTVar (active c) True {-# INLINE resumeAgentClient #-} -createUser :: AgentClient -> NonEmpty SMPServerWithAuth -> NonEmpty XFTPServerWithAuth -> AE UserId +createUser :: AgentClient -> NonEmpty (ServerCfg 'PSMP) -> NonEmpty (ServerCfg 'PXFTP) -> AE UserId createUser c = withAgentEnv c .: createUser' c {-# INLINE createUser #-} @@ -600,11 +604,13 @@ logConnection c connected = let event = if connected then "connected to" else "disconnected from" in logInfo $ T.unwords ["client", tshow (clientId c), event, "Agent"] -createUser' :: AgentClient -> NonEmpty SMPServerWithAuth -> NonEmpty XFTPServerWithAuth -> AM UserId +createUser' :: AgentClient -> NonEmpty (ServerCfg 'PSMP) -> NonEmpty (ServerCfg 'PXFTP) -> AM UserId createUser' c smp xftp = do + liftIO $ checkUserServers "createUser SMP" smp + liftIO $ checkUserServers "createUser XFTP" xftp userId <- withStore' c createUserRecord - atomically $ TM.insert userId smp $ smpServers c - atomically $ TM.insert userId xftp $ xftpServers c + atomically $ TM.insert userId (mkUserServers smp) $ smpServers c + atomically $ TM.insert userId (mkUserServers xftp) $ xftpServers c pure userId deleteUser' :: AgentClient -> UserId -> Bool -> AM () @@ -1815,10 +1821,17 @@ connectionStats = \case ratchetSyncSupported = connAgentVersion >= ratchetSyncSMPAgentVersion } --- | Change servers to be used for creating new queues, in Reader monad -setProtocolServers :: (ProtocolTypeI p, UserProtocol p) => AgentClient -> UserId -> NonEmpty (ProtoServerWithAuth p) -> IO () -setProtocolServers c userId srvs = atomically $ TM.insert userId srvs (userServers c) -{-# INLINE setProtocolServers #-} +-- | Change servers to be used for creating new queues. +-- This function will set all servers as enabled in case all passed servers are disabled. +setProtocolServers :: forall p. (ProtocolTypeI p, UserProtocol p) => AgentClient -> UserId -> NonEmpty (ServerCfg p) -> IO () +setProtocolServers c userId srvs = do + checkUserServers "setProtocolServers" srvs + atomically $ TM.insert userId (mkUserServers srvs) (userServers c) + +checkUserServers :: Text -> NonEmpty (ServerCfg p) -> IO () +checkUserServers name srvs = + unless (any (\ServerCfg {enabled} -> enabled) srvs) $ + logWarn (name <> ": all passed servers are disabled, using all servers.") registerNtfToken' :: AgentClient -> DeviceToken -> NotificationsMode -> AM NtfTknStatus registerNtfToken' c suppliedDeviceToken suppliedNtfMode = diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 0467c31f8..b8614c460 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -236,6 +236,7 @@ import Simplex.Messaging.Protocol ProtoServerWithAuth (..), Protocol (..), ProtocolServer (..), + ProtocolType (..), ProtocolTypeI (..), QueueId, QueueIdsKeys (..), @@ -289,7 +290,7 @@ data AgentClient = AgentClient active :: TVar Bool, subQ :: TBQueue ATransmission, msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg), - smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), + smpServers :: TMap UserId (UserServers 'PSMP), smpClients :: TMap SMPTransportSession SMPClientVar, -- smpProxiedRelays: -- SMPTransportSession defines connection from proxy to relay, @@ -297,7 +298,7 @@ data AgentClient = AgentClient smpProxiedRelays :: TMap SMPTransportSession SMPServerWithAuth, ntfServers :: TVar [NtfServer], ntfClients :: TMap NtfTransportSession NtfClientVar, - xftpServers :: TMap UserId (NonEmpty XFTPServerWithAuth), + xftpServers :: TMap UserId (UserServers 'PXFTP), xftpClients :: TMap XFTPTransportSession XFTPClientVar, useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), -- (slow, fast) networks userNetworkInfo :: TVar UserNetworkInfo, @@ -456,12 +457,12 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs a active <- newTVar True subQ <- newTBQueue qSize msgQ <- newTBQueue qSize - smpServers <- newTVar smp + smpServers <- newTVar $ M.map mkUserServers smp smpClients <- TM.empty smpProxiedRelays <- TM.empty ntfServers <- newTVar ntf ntfClients <- TM.empty - xftpServers <- newTVar xftp + xftpServers <- newTVar $ M.map mkUserServers xftp xftpClients <- TM.empty useNetworkConfig <- newTVar (slowNetworkConfig netCfg, netCfg) userNetworkInfo <- newTVar $ UserNetworkInfo UNOther True @@ -1069,7 +1070,7 @@ sendOrProxySMPCommand c userId destSrv cmdStr senderId sendCmdViaProxy sendCmdDi SPFAllow -> True SPFAllowProtected -> ipAddressProtected cfg destSrv SPFProhibit -> False - unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) + unknownServer = maybe True (notElem destSrv . knownSrvs) <$> TM.lookup userId (smpServers c) sendViaProxy destSess@(_, _, qId) = do r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do r' <- liftClient SMP (clientServer smp) $ sendCmdViaProxy smp proxySess @@ -1904,7 +1905,7 @@ storeError = \case SEDatabaseBusy e -> CRITICAL True $ B.unpack e e -> INTERNAL $ show e -userServers :: forall p. (ProtocolTypeI p, UserProtocol p) => AgentClient -> TMap UserId (NonEmpty (ProtoServerWithAuth p)) +userServers :: forall p. (ProtocolTypeI p, UserProtocol p) => AgentClient -> TMap UserId (UserServers p) userServers c = case protocolTypeI @p of SPSMP -> smpServers c SPXFTP -> xftpServers c @@ -1926,7 +1927,7 @@ getNextServer c userId usedSrvs = withUserServers c userId $ \srvs -> withUserServers :: forall p a. (ProtocolTypeI p, UserProtocol p) => AgentClient -> UserId -> (NonEmpty (ProtoServerWithAuth p) -> AM a) -> AM a withUserServers c userId action = atomically (TM.lookup userId $ userServers c) >>= \case - Just srvs -> action srvs + Just srvs -> action $ enabledSrvs srvs _ -> throwE $ INTERNAL "unknown userId - no user servers" withNextSrv :: forall p a. (ProtocolTypeI p, UserProtocol p) => AgentClient -> UserId -> TVar [ProtocolServer p] -> [ProtocolServer p] -> (ProtoServerWithAuth p -> AM a) -> AM a @@ -1935,7 +1936,7 @@ withNextSrv c userId usedSrvs initUsed action = do srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId used atomically $ do srvs_ <- TM.lookup userId $ userServers c - let unused = maybe [] ((\\ used) . map protoServer . L.toList) srvs_ + let unused = maybe [] ((\\ used) . map protoServer . L.toList . enabledSrvs) srvs_ used' = if null unused then initUsed else srv : used writeTVar usedSrvs $! used' action srvAuth diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 2ae2ad5c0..0f88508b9 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -7,6 +7,7 @@ {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} @@ -15,7 +16,12 @@ module Simplex.Messaging.Agent.Env.SQLite AM, AgentConfig (..), InitialAgentServers (..), + ServerCfg (..), + UserServers (..), NetworkConfig (..), + presetServerCfg, + enabledServerCfg, + mkUserServers, defaultAgentConfig, defaultReconnectInterval, tryAgentError, @@ -39,10 +45,14 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Crypto.Random +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson.TH as JQ import Data.ByteArray (ScrubbedBytes) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import Data.Map (Map) +import Data.Maybe (fromMaybe) import Data.Time.Clock (NominalDiffTime, nominalDay) import Data.Time.Clock.System (SystemTime (..)) import Data.Word (Word16) @@ -59,7 +69,8 @@ import Simplex.Messaging.Crypto.Ratchet (VersionRangeE2E, supportedE2EEncryptVRa import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Notifications.Transport (NTFVersion) import Simplex.Messaging.Notifications.Types -import Simplex.Messaging.Protocol (NtfServer, VersionRangeSMPC, XFTPServer, XFTPServerWithAuth, supportedSMPClientVRange) +import Simplex.Messaging.Parsers (defaultJSON) +import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth, ProtocolServer, ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..)) @@ -74,12 +85,38 @@ type AM' a = ReaderT Env IO a type AM a = ExceptT AgentErrorType (ReaderT Env IO) a data InitialAgentServers = InitialAgentServers - { smp :: Map UserId (NonEmpty SMPServerWithAuth), + { smp :: Map UserId (NonEmpty (ServerCfg 'PSMP)), ntf :: [NtfServer], - xftp :: Map UserId (NonEmpty XFTPServerWithAuth), + xftp :: Map UserId (NonEmpty (ServerCfg 'PXFTP)), netCfg :: NetworkConfig } +data ServerCfg p = ServerCfg + { server :: ProtoServerWithAuth p, + preset :: Bool, + tested :: Maybe Bool, + enabled :: Bool + } + deriving (Show) + +enabledServerCfg :: ProtoServerWithAuth p -> ServerCfg p +enabledServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True} + +presetServerCfg :: Bool -> ProtoServerWithAuth p -> ServerCfg p +presetServerCfg enabled server = ServerCfg {server, preset = True, tested = Nothing, enabled} + +data UserServers p = UserServers + { enabledSrvs :: NonEmpty (ProtoServerWithAuth p), + knownSrvs :: NonEmpty (ProtocolServer p) + } + +-- This function sets all servers as enabled in case all passed servers are disabled. +mkUserServers :: NonEmpty (ServerCfg p) -> UserServers p +mkUserServers srvs = UserServers {enabledSrvs, knownSrvs} + where + enabledSrvs = L.map (\ServerCfg {server} -> server) $ fromMaybe srvs $ L.nonEmpty $ L.filter (\ServerCfg {enabled} -> enabled) srvs + knownSrvs = L.map (\ServerCfg {server = ProtoServerWithAuth srv _} -> srv) srvs + data AgentConfig = AgentConfig { tcpPort :: Maybe ServiceName, rcvAuthAlg :: C.AuthAlg, @@ -294,3 +331,12 @@ updateRestartCount :: SystemTime -> RestartCount -> RestartCount updateRestartCount t (RestartCount minute count) = do let min' = systemSeconds t `div` 60 in RestartCount min' $ if minute == min' then count + 1 else 1 + +$(pure []) + +instance ProtocolTypeI p => ToJSON (ServerCfg p) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg) + toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg) + +instance ProtocolTypeI p => FromJSON (ServerCfg p) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerCfg) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index ab78d2ee9..1893ae105 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -980,7 +980,7 @@ testAsyncServerOffline t = withAgentClients2 $ \alice bob -> do testAllowConnectionClientRestart :: HasCallStack => ATransport -> IO () testAllowConnectionClientRestart t = do - let initAgentServersSrv2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer2]} + let initAgentServersSrv2 = initAgentServers {smp = userServers [testSMPServer2]} alice <- getSMPAgentClient' 1 agentCfg initAgentServers testDB bob <- getSMPAgentClient' 2 agentCfg initAgentServersSrv2 testDB2 withSmpServerStoreLogOn t testPort $ \_ -> do @@ -1335,7 +1335,7 @@ testExpireMessage t = testExpireManyMessages :: HasCallStack => ATransport -> IO () testExpireManyMessages t = - withAgent 1 agentCfg {messageTimeout = 1.5, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> + withAgent 1 agentCfg {messageTimeout = 2, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB $ \a -> withAgent 2 agentCfg initAgentServers testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ makeConnection a b runRight_ $ do @@ -1344,7 +1344,7 @@ testExpireManyMessages t = 2 <- sendMessage a bId SMP.noMsgFlags "1" 3 <- sendMessage a bId SMP.noMsgFlags "2" 4 <- sendMessage a bId SMP.noMsgFlags "3" - liftIO $ threadDelay 1500000 + liftIO $ threadDelay 2000000 5 <- sendMessage a bId SMP.noMsgFlags "4" -- this won't expire get a =##> \case ("", c, MERR 2 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False -- get a =##> \case ("", c, MERRS [5, 6] (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False @@ -1401,7 +1401,7 @@ testExpireMessageQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testP testExpireManyMessagesQuota :: ATransport -> IO () testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testPort $ \_ -> do - a <- getSMPAgentClient' 1 agentCfg {quotaExceededTimeout = 1, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB + a <- getSMPAgentClient' 1 agentCfg {quotaExceededTimeout = 2, messageRetryInterval = fastMessageRetryInterval} initAgentServers testDB b <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2 (aId, bId) <- runRight $ do (aId, bId) <- makeConnection a b @@ -1411,7 +1411,7 @@ testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} 3 <- sendMessage a bId SMP.noMsgFlags "2" 4 <- sendMessage a bId SMP.noMsgFlags "3" 5 <- sendMessage a bId SMP.noMsgFlags "4" - liftIO $ threadDelay 1000000 + liftIO $ threadDelay 2000000 6 <- sendMessage a bId SMP.noMsgFlags "5" -- this won't expire get a =##> \case ("", c, MERR 3 (SMP _ QUOTA)) -> bId == c; _ -> False get a >>= \case @@ -2226,7 +2226,7 @@ testWaitDeliveryTimeout2 t = testJoinConnectionAsyncReplyErrorV8 :: HasCallStack => ATransport -> IO () testJoinConnectionAsyncReplyErrorV8 t = do - let initAgentServersSrv2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer2]} + let initAgentServersSrv2 = initAgentServers {smp = userServers [testSMPServer2]} withAgent 1 agentCfgVPrevPQ initAgentServers testDB $ \a -> withAgent 2 agentCfgVPrevPQ initAgentServersSrv2 testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do @@ -2265,7 +2265,7 @@ testJoinConnectionAsyncReplyErrorV8 t = do testJoinConnectionAsyncReplyError :: HasCallStack => ATransport -> IO () testJoinConnectionAsyncReplyError t = do - let initAgentServersSrv2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer2]} + let initAgentServersSrv2 = initAgentServers {smp = userServers [testSMPServer2]} withAgent 1 agentCfg initAgentServers testDB $ \a -> withAgent 2 agentCfg initAgentServersSrv2 testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do @@ -2308,7 +2308,7 @@ testUsers = withAgentClients2 $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b exchangeGreetings a bId b aId - auId <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] + auId <- createUser a [noAuthSrvCfg testSMPServer] [noAuthSrvCfg testXFTPServer] (aId', bId') <- makeConnectionForUsers a auId b 1 exchangeGreetings a bId' b aId' deleteUser a auId True @@ -2323,7 +2323,7 @@ testDeleteUserQuietly = withAgentClients2 $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b exchangeGreetings a bId b aId - auId <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] + auId <- createUser a [noAuthSrvCfg testSMPServer] [noAuthSrvCfg testXFTPServer] (aId', bId') <- makeConnectionForUsers a auId b 1 exchangeGreetings a bId' b aId' deleteUser a auId False @@ -2335,7 +2335,7 @@ testUsersNoServer t = withAgentClientsCfg2 aCfg agentCfg $ \a b -> do (aId, bId, auId, _aId', bId') <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do (aId, bId) <- makeConnection a b exchangeGreetings a bId b aId - auId <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] + auId <- createUser a [noAuthSrvCfg testSMPServer] [noAuthSrvCfg testXFTPServer] (aId', bId') <- makeConnectionForUsers a auId b 1 exchangeGreetings a bId' b aId' pure (aId, bId, auId, aId', bId') @@ -2759,7 +2759,7 @@ testCreateQueueAuth srvVersion clnt1 clnt2 baseId = do pure r where getClient clientId (clntAuth, clntVersion) db = - let servers = initAgentServers {smp = userServers [ProtoServerWithAuth testSMPServer clntAuth]} + let servers = initAgentServers {smp = userServers' [ProtoServerWithAuth testSMPServer clntAuth]} alpn_ = if clntVersion >= authCmdsSMPVersion then Just supportedSMPHandshakes else Nothing smpCfg = defaultClientConfig alpn_ $ V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion sndAuthAlg = if srvVersion >= authCmdsSMPVersion && clntVersion >= authCmdsSMPVersion then C.AuthAlg C.SX25519 else C.AuthAlg C.SEd25519 @@ -2931,7 +2931,7 @@ testTwoUsers = withAgentClients2 $ \a b -> do ("", "", UP _ _) <- nGet a a `hasClients` 1 - aUserId2 <- createUser a [noAuthSrv testSMPServer] [noAuthSrv testXFTPServer] + aUserId2 <- createUser a [noAuthSrvCfg testSMPServer] [noAuthSrvCfg testXFTPServer] (aId2, bId2) <- makeConnectionForUsers a aUserId2 b 1 exchangeGreetings a bId2 b aId2 (aId2', bId2') <- makeConnectionForUsers a aUserId2 b 1 diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 7cb2a88c5..e0de57466 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -11,6 +11,7 @@ module SMPAgentClient where import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import NtfClient (ntfTestPort) @@ -20,7 +21,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, defaultNetworkConfig, defaultSMPClientConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) -import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) +import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth (..), ProtocolServer) import Simplex.Messaging.Transport import XFTPClient (testXFTPServer) @@ -48,14 +49,14 @@ testNtfServer2 = "ntf://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:6 initAgentServers :: InitialAgentServers initAgentServers = InitialAgentServers - { smp = userServers [noAuthSrv testSMPServer], + { smp = userServers [testSMPServer], ntf = [testNtfServer], - xftp = userServers [noAuthSrv testXFTPServer], + xftp = userServers [testXFTPServer], netCfg = defaultNetworkConfig {tcpTimeout = 500_000, tcpConnectTimeout = 500_000} } initAgentServers2 :: InitialAgentServers -initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]} +initAgentServers2 = initAgentServers {smp = userServers [testSMPServer, testSMPServer2]} initAgentServersProxy :: SMPProxyMode -> SMPProxyFallback -> InitialAgentServers initAgentServersProxy smpProxyMode smpProxyFallback = @@ -89,5 +90,11 @@ fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} fastMessageRetryInterval :: RetryInterval2 fastMessageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} -userServers :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ProtoServerWithAuth p)) -userServers srvs = M.fromList [(1, srvs)] +userServers :: NonEmpty (ProtocolServer p) -> Map UserId (NonEmpty (ServerCfg p)) +userServers = userServers' . L.map noAuthSrv + +userServers' :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ServerCfg p)) +userServers' srvs = M.fromList [(1, L.map (presetServerCfg True) srvs)] + +noAuthSrvCfg :: ProtocolServer p -> ServerCfg p +noAuthSrvCfg = presetServerCfg True . noAuthSrv diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 6452d2677..625dfbda7 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -234,7 +234,7 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b where msgId = subtract baseId . fst aCfg = agentCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} - servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers srvs} agentDeliverMessagesViaProxyConc :: [NonEmpty SMPServer] -> [MsgBody] -> IO () agentDeliverMessagesViaProxyConc agentServers msgs = @@ -299,7 +299,7 @@ agentDeliverMessagesViaProxyConc agentServers msgs = logDebug "run finished" pqEnc = CR.PQEncOn aCfg = agentCfg {sndAuthAlg = C.AuthAlg C.SEd448, rcvAuthAlg = C.AuthAlg C.SEd448} - servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers srvs} agentViaProxyVersionError :: IO () agentViaProxyVersionError = @@ -310,7 +310,7 @@ agentViaProxyVersionError = A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe pure () where - servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs} + servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers srvs} agentViaProxyRetryOffline :: IO () agentViaProxyRetryOffline = do @@ -372,7 +372,7 @@ agentViaProxyRetryOffline = do aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval} baseId = 1 msgId = subtract baseId . fst - servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]} + servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers [srv]} testNoProxy :: IO () testNoProxy = do From 8dd54ced0eb344e04298acf4a6b2d71e54cede01 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 13 Jul 2024 10:06:48 +0100 Subject: [PATCH 121/125] agent: retry proxied command on NO_SESSION error, to prevent failure of proxied interactive commands (like joining connection) (#1227) --- src/Simplex/Messaging/Agent/Client.hs | 45 ++++++++++++++++----------- src/Simplex/Messaging/Client.hs | 5 +-- tests/SMPProxyTests.hs | 31 +++++++++++++++--- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index b8614c460..43b3b8064 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -597,10 +597,10 @@ getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do prs <- atomically TM.empty smpConnectClient c tSess prs v -getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) -getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do +getSMPProxyClient :: AgentClient -> Maybe SMPServerWithAuth -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) +getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} proxySrv_ destSess@(userId, destSrv, qId) = do unlessM (readTVarIO active) $ throwE INACTIVE - proxySrv <- getNextServer c userId [destSrv] + proxySrv <- maybe (getNextServer c userId [destSrv]) pure proxySrv_ ts <- liftIO getCurrentTime atomically (getClientVar proxySrv ts) >>= \(tSess, auth, v) -> either (newProxyClient tSess auth ts) (waitForProxyClient tSess auth) v @@ -993,9 +993,9 @@ withClient_ c tSess@(_, srv, _) action = do logServer "<--" c srv "" $ bshow e throwE e -withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a -withProxySession c destSess@(_, destSrv, _) entId cmdStr action = do - (cl, sess_) <- getSMPProxyClient c destSess +withProxySession :: AgentClient -> Maybe SMPServerWithAuth -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a +withProxySession c proxySrv_ destSess@(_, destSrv, _) entId cmdStr action = do + (cl, sess_) <- getSMPProxyClient c proxySrv_ destSess logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr case sess_ of Right sess -> do @@ -1053,7 +1053,7 @@ sendOrProxySMPCommand :: AM (Maybe SMPServer) sendOrProxySMPCommand c userId destSrv cmdStr senderId sendCmdViaProxy sendCmdDirectly = do sess <- liftIO $ mkTransportSession c userId destSrv senderId - ifM (atomically shouldUseProxy) (sendViaProxy sess) (sendDirectly sess $> Nothing) + ifM (atomically shouldUseProxy) (sendViaProxy Nothing sess) (sendDirectly sess $> Nothing) where shouldUseProxy = do cfg <- getNetworkConfig c @@ -1071,22 +1071,31 @@ sendOrProxySMPCommand c userId destSrv cmdStr senderId sendCmdViaProxy sendCmdDi SPFAllowProtected -> ipAddressProtected cfg destSrv SPFProhibit -> False unknownServer = maybe True (notElem destSrv . knownSrvs) <$> TM.lookup userId (smpServers c) - sendViaProxy destSess@(_, _, qId) = do - r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do + sendViaProxy :: Maybe SMPServerWithAuth -> SMPTransportSession -> AM (Maybe SMPServer) + sendViaProxy proxySrv_ destSess@(_, _, qId) = do + r <- tryAgentError . withProxySession c proxySrv_ destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess@ProxiedRelay {prBasicAuth}) -> do r' <- liftClient SMP (clientServer smp) $ sendCmdViaProxy smp proxySess + let proxySrv = protocolClientServer' smp case r' of - Right () -> pure . Just $ protocolClientServer' smp + Right () -> pure $ Just proxySrv Left proxyErr -> do case proxyErr of - (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> atomically deleteRelaySession - _ -> pure () - throwE - PROXY - { proxyServer = protocolClientServer smp, - relayServer = B.unpack $ strEncode destSrv, - proxyErr - } + ProxyProtocolError (SMP.PROXY SMP.NO_SESSION) -> do + atomically deleteRelaySession + case proxySrv_ of + Just _ -> proxyError + -- sendViaProxy is called recursively here to re-create the session via the same server + -- to avoid failure in interactive calls that don't retry after the session disconnection. + Nothing -> sendViaProxy (Just $ ProtoServerWithAuth proxySrv prBasicAuth) destSess + _ -> proxyError where + proxyError = + throwE + PROXY + { proxyServer = protocolClientServer smp, + relayServer = B.unpack $ strEncode destSrv, + proxyErr + } -- checks that the current proxied relay session is the same one that was used to send the message and removes it deleteRelaySession = ( TM.lookup destSess (smpProxiedRelays c) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 1837a256b..4b1e673b0 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -823,7 +823,7 @@ connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, t PKEY sId vr (chain, key) -> case supportedClientSMPRelayVRange `compatibleVersion` vr of Nothing -> throwE $ transportErr TEVersion - Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key + Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v proxyAuth <$> validateRelay chain key r -> throwE $ unexpectedResponse r | otherwise = throwE $ PCETransportError TEVersion where @@ -842,6 +842,7 @@ connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, t data ProxiedRelay = ProxiedRelay { prSessionId :: SessionId, prVersion :: VersionSMP, + prBasicAuth :: Maybe BasicAuth, -- auth is included here to allow reconnecting via the same proxy after NO_SESSION error prServerKey :: C.PublicKeyX25519 } @@ -902,7 +903,7 @@ proxySMPCommand :: SenderId -> Command 'Sender -> ExceptT SMPClientError IO (Either ProxyClientError ()) -proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId command = do +proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v _ serverKey) spKey sId command = do -- prepare params let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 625dfbda7..7505ef977 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -34,7 +34,8 @@ import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Protocol (EncRcvMsgBody (..), MsgBody, RcvMessage (..), SubscriptionMode (..), maxMessageLength, noMsgFlags) +import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Util (bshow, tshow) @@ -122,6 +123,8 @@ smpProxyTests = do agentViaProxyVersionError it "retries sending when destination or proxy relay is offline" $ agentViaProxyRetryOffline + it "retries sending when destination relay session disconnects in proxy" $ + agentViaProxyRetryNoSession describe "stress test 1k" $ do let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) it "2 agents, 250 messages" . oneServer $ deliver 2 250 @@ -157,7 +160,7 @@ deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do -- prepare receiving queue (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe False + SMP.QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe False let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv -- get proxy session sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") @@ -374,18 +377,38 @@ agentViaProxyRetryOffline = do msgId = subtract baseId . fst servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers [srv]} +agentViaProxyRetryNoSession :: IO () +agentViaProxyRetryNoSession = do + let srv1 = SMPServer testHost testPort testKeyHash + srv2 = SMPServer testHost testPort2 testKeyHash + withAgent 1 agentCfg (servers srv1) testDB $ \a -> + withAgent 2 agentCfg (servers srv2) testDB2 $ \b -> do + withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do + (aId, _) <- withServer2 $ \_ -> runRight $ makeConnection a b + nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False + withServer2 $ \_ -> do + nGet b =##> \case ("", "", UP _ [c]) -> c == aId; _ -> False + -- to test retry in case of NO_SESSION error, + -- the client using server 1 as proxy and server 2 as destination + -- should be joining the connection, so the order is swapped here. + _ <- runRight $ makeConnection b a + pure () + where + withServer2 = withSmpServerConfigOn (transport @TLS) proxyCfg {storeLogFile = Just testStoreLogFile2, storeMsgsFile = Just testStoreMsgsFile2} testPort2 + servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers [srv]} + testNoProxy :: IO () testNoProxy = do withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do testSMPClient_ "127.0.0.1" testPort2 proxyVRangeV8 $ \(th :: THandleSMP TLS 'TClient) -> do - (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) + (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", SMP.PRXY testSMPServer Nothing) reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) testProxyAuth :: IO () testProxyAuth = do withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do testSMPClient_ "127.0.0.1" testPort proxyVRangeV8 $ \(th :: THandleSMP TLS 'TClient) -> do - (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") + (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", SMP.PRXY testSMPServer2 $ Just "wrong") reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) where proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} From 492d2f86bc5fa23f33f0b5e743c9b44e1d19049b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 13 Jul 2024 22:34:10 +0100 Subject: [PATCH 122/125] smp server: additional control port commands to monitor server state (#1228) * smp server: additional control port commands to monitor server state * fix * space --- src/Simplex/Messaging/Server.hs | 46 ++++++++++++++++++++++--- src/Simplex/Messaging/Server/Control.hs | 4 +++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 014bcd366..d88b2349a 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -6,7 +6,6 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} @@ -60,6 +59,7 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) +import qualified Data.Set as S import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) @@ -377,7 +377,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do let age = systemSeconds now - systemSeconds createdAt subscriptions' <- bshow . M.size <$> readTVarIO subscriptions hPutStrLn h . B.unpack $ B.intercalate "," [bshow cid, encode sessionId, connected', strEncode createdAt, rcvActiveAt', sndActiveAt', bshow age, subscriptions'] - CPStats -> withAdminRole $ do + CPStats -> withUserRole $ do ss <- unliftIO u $ asks serverStats let putStat :: Show a => ByteString -> (ServerStats -> TVar a) -> IO () putStat label var = readTVarIO (var ss) >>= \v -> B.hPutStr h $ label <> ": " <> bshow v <> "\n" @@ -391,6 +391,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do putStat "qDeletedAll" qDeletedAll putStat "qDeletedNew" qDeletedNew putStat "qDeletedSecured" qDeletedSecured + readTVarIO (day $ activeQueues ss) >>= \v -> B.hPutStr h $ "dayMsgQueues" <> ": " <> bshow (S.size v) <> "\n" putStat "msgSent" msgSent putStat "msgRecv" msgRecv putStat "msgSentNtf" msgSentNtf @@ -414,7 +415,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do #else hPutStrLn h "Not available on GHC 8.10" #endif - CPSockets -> withAdminRole $ do + CPSockets -> withUserRole $ do (accepted', closed', active') <- unliftIO u $ asks sockets (accepted, closed, active) <- atomically $ (,,) <$> readTVar accepted' <*> readTVar closed' <*> readTVar active' hPutStrLn h "Sockets: " @@ -436,6 +437,43 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do #else hPutStrLn h "Not available on GHC 8.10" #endif + CPServerInfo -> readTVarIO role >>= \case + CPRNone -> do + logError "Unauthorized control port command" + hPutStrLn h "AUTH" + r -> do +#if MIN_VERSION_base(4,18,0) + threads <- liftIO listThreads + hPutStrLn h $ "Threads: " <> show (length threads) +#else + hPutStrLn h "Threads: not available on GHC 8.10" +#endif + Env {clients, server = Server {subscribers, notifiers}} <- unliftIO u ask + activeClients <- readTVarIO clients + hPutStrLn h $ "Clients: " <> show (IM.size activeClients) + when (r == CPRAdmin) $ do + (smpSubCnt, smpClCnt) <- countClientSubs subscriptions activeClients + (ntfSubCnt, ntfClCnt) <- countClientSubs ntfSubscriptions activeClients + hPutStrLn h $ "SMP subscriptions (via clients, slow): " <> show smpSubCnt + hPutStrLn h $ "SMP subscribed clients (via clients, slow): " <> show smpClCnt + hPutStrLn h $ "Ntf subscriptions (via clients, slow): " <> show ntfSubCnt + hPutStrLn h $ "Ntf subscribed clients (via clients, slow): " <> show ntfClCnt + activeSubs <- readTVarIO subscribers + activeNtfSubs <- readTVarIO notifiers + hPutStrLn h $ "SMP subscriptions: " <> show (M.size activeSubs) + hPutStrLn h $ "SMP subscribed clients: " <> show (countSubClients activeSubs) + hPutStrLn h $ "Ntf subscriptions: " <> show (M.size activeNtfSubs) + hPutStrLn h $ "Ntf subscribed clients: " <> show (countSubClients activeNtfSubs) + where + countClientSubs :: (Client -> TMap QueueId a) -> IM.IntMap Client -> IO (Int, Int) + countClientSubs subSel = foldM addSubs (0, 0) + where + addSubs :: (Int, Int) -> Client -> IO (Int, Int) + addSubs (subCnt, clCnt) cl = do + subs <- readTVarIO $ subSel cl + let cnt = M.size subs + pure (subCnt + cnt, clCnt + if cnt == 0 then 0 else 1) + countSubClients = S.size . M.foldr' (S.insert . clientId) S.empty CPDelete queueId' -> withUserRole $ unliftIO u $ do st <- asks queueStore ms <- asks msgStore @@ -455,7 +493,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do hPutStrLn h "saving server state..." unliftIO u $ saveServer True hPutStrLn h "server state saved!" - CPHelp -> hPutStrLn h "commands: stats, stats-rts, clients, sockets, socket-threads, threads, delete, save, help, quit" + CPHelp -> hPutStrLn h "commands: stats, stats-rts, clients, sockets, socket-threads, threads, server-info, delete, save, help, quit" CPQuit -> pure () CPSkip -> pure () where diff --git a/src/Simplex/Messaging/Server/Control.hs b/src/Simplex/Messaging/Server/Control.hs index 9463fa777..b4c74e4ac 100644 --- a/src/Simplex/Messaging/Server/Control.hs +++ b/src/Simplex/Messaging/Server/Control.hs @@ -9,6 +9,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (BasicAuth) data CPClientRole = CPRNone | CPRUser | CPRAdmin + deriving (Eq) data ControlProtocol = CPAuth BasicAuth @@ -20,6 +21,7 @@ data ControlProtocol | CPThreads | CPSockets | CPSocketThreads + | CPServerInfo | CPDelete ByteString | CPSave | CPHelp @@ -37,6 +39,7 @@ instance StrEncoding ControlProtocol where CPThreads -> "threads" CPSockets -> "sockets" CPSocketThreads -> "socket-threads" + CPServerInfo -> "server-info" CPDelete bs -> "delete " <> strEncode bs CPSave -> "save" CPHelp -> "help" @@ -53,6 +56,7 @@ instance StrEncoding ControlProtocol where "threads" -> pure CPThreads "sockets" -> pure CPSockets "socket-threads" -> pure CPSocketThreads + "server-info" -> pure CPServerInfo "delete" -> CPDelete <$> (A.space *> strP) "save" -> pure CPSave "help" -> pure CPHelp From d4fa0af350dc44edbeaeff1d4881be1c830cb94b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 14 Jul 2024 17:57:34 +0100 Subject: [PATCH 123/125] ntf: additional tests for token registration when server and device are restarted (#1230) * ntf: additional tests for token registration when server and device are restarted * test response timeouts --- tests/AgentTests/NotificationTests.hs | 152 ++++++++++++++++++++++++-- 1 file changed, 141 insertions(+), 11 deletions(-) diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index a104c6cf5..92d97d641 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -5,6 +5,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} @@ -49,6 +50,7 @@ import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Text.Encoding (encodeUtf8) +import Database.SQLite.Simple.QQ (sql) import NtfClient import SMPAgentClient (agentCfg, initAgentServers, initAgentServers2, testDB, testDB2, testNtfServer, testNtfServer2) import SMPClient (cfg, cfgVPrev, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn) @@ -56,13 +58,14 @@ import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMes import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore') import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers) import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT) -import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore, getSavedNtfToken, reopenSQLiteStore) +import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore, getSavedNtfToken, reopenSQLiteStore, withTransaction) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Server.Push.APNS -import Simplex.Messaging.Notifications.Types (NtfToken (..)) +import Simplex.Messaging.Notifications.Types (NtfTknAction (..), NtfToken (..)) import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) @@ -88,9 +91,21 @@ notificationTests t = do it "should allow the second registration with different credentials and delete the first after verification" $ withAPNSMockServer $ \apns -> withNtfServer t $ testNtfTokenSecondRegistration apns - it "should re-register token when notification server is restarted" $ + it "should verify token after notification server is restarted" $ withAPNSMockServer $ \apns -> testNtfTokenServerRestart t apns + it "should re-verify token after notification server is restarted" $ + withAPNSMockServer $ \apns -> + testNtfTokenServerRestartReverify t apns + it "should re-verify token after notification server is restarted when first request timed-out" $ + withAPNSMockServer $ \apns -> + testNtfTokenServerRestartReverifyTimeout t apns + it "should re-register token when notification server is restarted" $ + withAPNSMockServer $ \apns -> + testNtfTokenServerRestartReregister t apns + it "should re-register token when notification server is restarted when first request timed-out" $ + withAPNSMockServer $ \apns -> + testNtfTokenServerRestartReregisterTimeout t apns it "should work with multiple configured servers" $ withAPNSMockServer $ \apns -> testNtfTokenMultipleServers t apns @@ -251,7 +266,7 @@ testNtfTokenServerRestart :: ATransport -> APNSMockServer -> IO () testNtfTokenServerRestart t APNSMockServer {apnsQ} = do let tkn = DeviceToken PPApnsTest "abcd" ntfData <- withAgent 1 agentCfg initAgentServers testDB $ \a -> - withNtfServer t . runRight $ do + withNtfServerStoreLog t $ \_ -> runRight $ do NTRegistered <- registerNtfToken a tkn NMPeriodic APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <- atomically $ readTBQueue apnsQ @@ -262,16 +277,131 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do withAgent 2 agentCfg initAgentServers testDB $ \a' -> -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, -- so that repeat verification happens without restarting the clients, when notification arrives - withNtfServer t . runRight_ $ do + withNtfServerStoreLog t $ \_ -> runRight_ $ do verification <- ntfData .-> "verification" nonce <- C.cbNonce <$> ntfData .-> "nonce" - Left (NTF _ AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification - APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <- + verifyNtfToken a' tkn nonce verification + NTActive <- checkNtfToken a' tkn + pure () + +testNtfTokenServerRestartReverify :: ATransport -> APNSMockServer -> IO () +testNtfTokenServerRestartReverify t APNSMockServer {apnsQ} = do + let tkn = DeviceToken PPApnsTest "abcd" + withAgent 1 agentCfg initAgentServers testDB $ \a -> do + ntfData <- withNtfServerStoreLog t $ \_ -> runRight $ do + NTRegistered <- registerNtfToken a tkn NMPeriodic + APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <- atomically $ readTBQueue apnsQ - verification' <- ntfData' .-> "verification" - nonce' <- C.cbNonce <$> ntfData' .-> "nonce" - liftIO $ sendApnsResponse' APNSRespOk - verifyNtfToken a' tkn nonce' verification' + liftIO $ sendApnsResponse APNSRespOk + pure ntfData + runRight_ $ do + verification <- ntfData .-> "verification" + nonce <- C.cbNonce <$> ntfData .-> "nonce" + Left (BROKER _ NETWORK) <- tryE $ verifyNtfToken a tkn nonce verification + pure () + threadDelay 1000000 + withAgent 2 agentCfg initAgentServers testDB $ \a' -> + -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, + -- so that repeat verification happens without restarting the clients, when notification arrives + withNtfServerStoreLog t $ \_ -> runRight_ $ do + NTActive <- registerNtfToken a' tkn NMPeriodic + NTActive <- checkNtfToken a' tkn + pure () + +testNtfTokenServerRestartReverifyTimeout :: ATransport -> APNSMockServer -> IO () +testNtfTokenServerRestartReverifyTimeout t APNSMockServer {apnsQ} = do + let tkn = DeviceToken PPApnsTest "abcd" + withAgent 1 agentCfg initAgentServers testDB $ \a@AgentClient {agentEnv = Env {store}} -> do + (nonce, verification) <- withNtfServerStoreLog t $ \_ -> runRight $ do + NTRegistered <- registerNtfToken a tkn NMPeriodic + APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <- + atomically $ readTBQueue apnsQ + liftIO $ sendApnsResponse APNSRespOk + verification <- ntfData .-> "verification" + nonce <- C.cbNonce <$> ntfData .-> "nonce" + verifyNtfToken a tkn nonce verification + pure (nonce, verification) + -- this emulates the situation when server verified token but the client did not receive the response + Just NtfToken {ntfTknStatus = NTActive, ntfTknAction = Just NTACheck, ntfDhSecret = Just dhSecret} <- withTransaction store getSavedNtfToken + Right code <- pure $ NtfRegCode <$> C.cbDecrypt dhSecret nonce verification + withTransaction store $ \db -> + DB.execute + db + [sql| + UPDATE ntf_tokens + SET tkn_status = ?, tkn_action = ? + WHERE provider = ? AND device_token = ? + |] + (NTConfirmed, Just (NTAVerify code), PPApnsTest, "abcd" :: ByteString) + Just NtfToken {ntfTknStatus = NTConfirmed, ntfTknAction = Just (NTAVerify _)} <- withTransaction store getSavedNtfToken + pure () + threadDelay 1000000 + withAgent 2 agentCfg initAgentServers testDB $ \a' -> + -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, + -- so that repeat verification happens without restarting the clients, when notification arrives + withNtfServerStoreLog t $ \_ -> runRight_ $ do + NTActive <- registerNtfToken a' tkn NMPeriodic + NTActive <- checkNtfToken a' tkn + pure () + +testNtfTokenServerRestartReregister :: ATransport -> APNSMockServer -> IO () +testNtfTokenServerRestartReregister t APNSMockServer {apnsQ} = do + let tkn = DeviceToken PPApnsTest "abcd" + withAgent 1 agentCfg initAgentServers testDB $ \a -> + withNtfServerStoreLog t $ \_ -> runRight $ do + NTRegistered <- registerNtfToken a tkn NMPeriodic + APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just _}, sendApnsResponse} <- + atomically $ readTBQueue apnsQ + liftIO $ sendApnsResponse APNSRespOk + -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server + threadDelay 1000000 + withAgent 2 agentCfg initAgentServers testDB $ \a' -> + -- server stopped before token is verified, and client might have lost verification notification. + -- so that repeat registration happens when client is restarted. + withNtfServerStoreLog t $ \_ -> runRight_ $ do + NTRegistered <- registerNtfToken a' tkn NMPeriodic + APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <- + atomically $ readTBQueue apnsQ + liftIO $ sendApnsResponse APNSRespOk + verification <- ntfData .-> "verification" + nonce <- C.cbNonce <$> ntfData .-> "nonce" + verifyNtfToken a' tkn nonce verification + NTActive <- checkNtfToken a' tkn + pure () + +testNtfTokenServerRestartReregisterTimeout :: ATransport -> APNSMockServer -> IO () +testNtfTokenServerRestartReregisterTimeout t APNSMockServer {apnsQ} = do + let tkn = DeviceToken PPApnsTest "abcd" + withAgent 1 agentCfg initAgentServers testDB $ \a@AgentClient {agentEnv = Env {store}} -> do + withNtfServerStoreLog t $ \_ -> runRight $ do + NTRegistered <- registerNtfToken a tkn NMPeriodic + APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just _}, sendApnsResponse} <- + atomically $ readTBQueue apnsQ + liftIO $ sendApnsResponse APNSRespOk + -- this emulates the situation when server registered token but the client did not receive the response + withTransaction store $ \db -> + DB.execute + db + [sql| + UPDATE ntf_tokens + SET tkn_id = NULL, tkn_dh_secret = NULL, tkn_status = ?, tkn_action = ? + WHERE provider = ? AND device_token = ? + |] + (NTNew, Just NTARegister, PPApnsTest, "abcd" :: ByteString) + Just NtfToken {ntfTokenId = Nothing, ntfTknStatus = NTNew, ntfTknAction = Just NTARegister} <- withTransaction store getSavedNtfToken + pure () + threadDelay 1000000 + withAgent 2 agentCfg initAgentServers testDB $ \a' -> + -- server stopped before token is verified, and client might have lost verification notification. + -- so that repeat registration happens when client is restarted. + withNtfServerStoreLog t $ \_ -> runRight_ $ do + NTRegistered <- registerNtfToken a' tkn NMPeriodic + APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <- + atomically $ readTBQueue apnsQ + liftIO $ sendApnsResponse APNSRespOk + verification <- ntfData .-> "verification" + nonce <- C.cbNonce <$> ntfData .-> "nonce" + verifyNtfToken a' tkn nonce verification NTActive <- checkNtfToken a' tkn pure () From 291039159fbba8e3aa0ba244890a4f1085756cfc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 14 Jul 2024 23:19:02 +0100 Subject: [PATCH 124/125] ntf server: use SOCKS proxy to connect to onion-only SMP servers (#1229) * ntf server: use SOCKS proxy to connect to onion-only SMP servers * fix test --- src/Simplex/Messaging/Client.hs | 2 +- .../Messaging/Notifications/Server/Main.hs | 23 ++++++++++++++++++- src/Simplex/Messaging/Server/CLI.hs | 7 ++++++ src/Simplex/Messaging/Server/Main.hs | 5 ---- tests/AgentTests/FunctionalAPITests.hs | 3 +-- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 4b1e673b0..4cfef92dd 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -240,7 +240,7 @@ data SocksMode = -- | always use SOCKS proxy when enabled SMAlways | -- | use SOCKS proxy only for .onion hosts when no public host is available - -- This mode is used in SMP proxy to minimize SOCKS proxy usage. + -- This mode is used in SMP proxy and in notifications server to minimize SOCKS proxy usage. SMOnion deriving (Eq, Show) diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index 351fe6d72..477b12bfc 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -7,6 +7,7 @@ module Simplex.Messaging.Notifications.Server.Main where +import Control.Monad ((<$!>)) import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Maybe (fromMaybe) @@ -14,6 +15,7 @@ import qualified Data.Text as T import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig) import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Server (runNtfServer) @@ -87,6 +89,14 @@ ntfServerCLI cfgPath logPath = <> ("port: " <> T.pack defaultServerPort <> "\n") <> "log_tls_errors: off\n" <> "websockets: off\n\n\ + \[SUBSCRIBER]\n\ + \# Network configuration for notification server client.\n\ + \# SOCKS proxy port for subscribing to SMP servers.\n\ + \# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\ + \# socks_proxy: localhost:9050\n\n\ + \# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\ + \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ + \# socks_mode: onion\n\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" @@ -115,7 +125,18 @@ ntfServerCLI cfgPath logPath = clientQSize = 64, subQSize = 512, pushQSize = 1048, - smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 0}, + smpAgentCfg = + defaultSMPClientAgentConfig + { smpCfg = + (smpCfg defaultSMPClientAgentConfig) + { networkConfig = + defaultNetworkConfig + { socksProxy = either error id <$!> strDecodeIni "SUBSCRIBER" "socks_proxy" ini, + socksMode = either (const SMOnion) textToSocksMode $ lookupValue "SUBSCRIBER" "socks_mode" ini + } + }, + persistErrorInterval = 0 -- seconds + }, apnsConfig = defaultAPNSPushClientConfig, subsBatchSize = 900, inactiveClientExpiration = diff --git a/src/Simplex/Messaging/Server/CLI.hs b/src/Simplex/Messaging/Server/CLI.hs index 956b30816..c22c1a161 100644 --- a/src/Simplex/Messaging/Server/CLI.hs +++ b/src/Simplex/Messaging/Server/CLI.hs @@ -24,6 +24,7 @@ import qualified Data.X509.File as XF import Data.X509.Validation (Fingerprint (..)) import Network.Socket (HostName, ServiceName) import Options.Applicative +import Simplex.Messaging.Client (SocksMode (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI) import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..)) @@ -301,3 +302,9 @@ clearDirIfExists path = whenM (doesDirectoryExist path) $ listDirectory path >>= getEnvPath :: String -> FilePath -> IO FilePath getEnvPath name def = maybe def (\case "" -> def; f -> f) <$> lookupEnv name + +textToSocksMode :: Text -> SocksMode +textToSocksMode = \case + "always" -> SMAlways + "onion" -> SMOnion + s -> error . T.unpack $ "Invalid socks_mode: " <> s diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 04e14544c..a135565bf 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -317,11 +317,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini, information = serverPublicInfo ini } - textToSocksMode :: Text -> SocksMode - textToSocksMode = \case - "always" -> SMAlways - "onion" -> SMOnion - s -> error . T.unpack $ "Invalid socks_mode: " <> s textToHostMode :: Text -> HostMode textToHostMode = \case "public" -> HMPublic diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 1893ae105..3f70ad6ab 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -1347,12 +1347,11 @@ testExpireManyMessages t = liftIO $ threadDelay 2000000 5 <- sendMessage a bId SMP.noMsgFlags "4" -- this won't expire get a =##> \case ("", c, MERR 2 (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False - -- get a =##> \case ("", c, MERRS [5, 6] (BROKER _ e)) -> bId == c && (e == TIMEOUT || e == NETWORK); _ -> False let expected c e = bId == c && (e == TIMEOUT || e == NETWORK) get a >>= \case ("", c, MERR 3 (BROKER _ e)) -> do liftIO $ expected c e `shouldBe` True - get a =##> \case ("", c', MERR 4 (BROKER _ e')) -> expected c' e'; ("", c', MERRS [6] (BROKER _ e')) -> expected c' e'; _ -> False + get a =##> \case ("", c', MERR 4 (BROKER _ e')) -> expected c' e'; ("", c', MERRS [4] (BROKER _ e')) -> expected c' e'; _ -> False ("", c, MERRS [3] (BROKER _ e)) -> do liftIO $ expected c e `shouldBe` True get a =##> \case ("", c', MERR 4 (BROKER _ e')) -> expected c' e'; _ -> False From 1bdfc8ae00c201eaad97d13c7ab55bd7539984f4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 14 Jul 2024 23:21:14 +0100 Subject: [PATCH 125/125] 6.0.0.1 --- package.yaml | 2 +- simplexmq.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index fbf95a7e5..303e84ef7 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 6.0.0.0 +version: 6.0.0.1 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index b7b1ca76c..0bfd62ce3 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 6.0.0.0 +version: 6.0.0.1 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and