mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-12 11:39:56 +00:00
Compare commits
80
Commits
v2.3.0
...
v3.0.0-beta.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e622e12a7f | ||
|
|
6b6ea78eac | ||
|
|
238a2e7fe9 | ||
|
|
82eff6c757 | ||
|
|
0392f849aa | ||
|
|
16f5fdeeb0 | ||
|
|
c3012173e3 | ||
|
|
f6a321e198 | ||
|
|
c82fae72f2 | ||
|
|
9695786aa5 | ||
|
|
56ef95e8ae | ||
|
|
79d67694ed | ||
|
|
8dc051179a | ||
|
|
0b25b57e3c | ||
|
|
07bf2bca3b | ||
|
|
6c14372666 | ||
|
|
2c7cad5378 | ||
|
|
c9501a23a0 | ||
|
|
926e515bc0 | ||
|
|
09b51dc44f | ||
|
|
85d507d5d3 | ||
|
|
b0ac0744e2 | ||
|
|
ba40d75886 | ||
|
|
0c7c0d066d | ||
|
|
3d6ad64d62 | ||
|
|
51d0b48ce1 | ||
|
|
a8260290e7 | ||
|
|
2809963a2e | ||
|
|
d2321b750d | ||
|
|
c2f1dabcd0 | ||
|
|
9e0e6b1afc | ||
|
|
58622c6aaf | ||
|
|
b82452fe03 | ||
|
|
70e7c71762 | ||
|
|
a92827c986 | ||
|
|
f10e3f697c | ||
|
|
ef4d4c9e16 | ||
|
|
ffb4b4763c | ||
|
|
0d9d549cea | ||
|
|
a7c3133c35 | ||
|
|
5b61c6bccd | ||
|
|
d78fb8761e | ||
|
|
cd9cb19765 | ||
|
|
30b36cd756 | ||
|
|
08a4b41691 | ||
|
|
756bd1eca7 | ||
|
|
7a72488275 | ||
|
|
7fa4862a83 | ||
|
|
0ab44b1836 | ||
|
|
d1db7d6f79 | ||
|
|
ec70429106 | ||
|
|
31c78e516d | ||
|
|
b58523d4a2 | ||
|
|
9f981042b9 | ||
|
|
8c2227ccce | ||
|
|
bca44ae54a | ||
|
|
50ddcd3a13 | ||
|
|
3336f987c3 | ||
|
|
1d56ef3a60 | ||
|
|
0c45e0bf76 | ||
|
|
5ccebaeb21 | ||
|
|
5f94240ed4 | ||
|
|
331918bac7 | ||
|
|
f7bd8a3982 | ||
|
|
68c2682e70 | ||
|
|
62485b9367 | ||
|
|
91802fe94f | ||
|
|
9c12b4edc0 | ||
|
|
c1348aa54f | ||
|
|
4220c3bdaf | ||
|
|
bfb556c860 | ||
|
|
3f985e8fd7 | ||
|
|
60294521f4 | ||
|
|
4b3d04bd27 | ||
|
|
0dc34cd287 | ||
|
|
46ba047077 | ||
|
|
facb6f4561 | ||
|
|
6a27b82759 | ||
|
|
b5ccce0f6a | ||
|
|
b60408837a |
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Main where
|
||||
@@ -41,7 +42,7 @@ ntfServerCLIConfig =
|
||||
fingerprintFile = combine cfgPath "fingerprint",
|
||||
defaultServerPort = "443",
|
||||
executableName = "ntf-server",
|
||||
serverVersion = "SMP notifications server v0.1.0",
|
||||
serverVersion = "SMP notifications server v1.0.0-beta.2",
|
||||
mkIniFile = \enableStoreLog defaultServerPort ->
|
||||
"[STORE_LOG]\n\
|
||||
\# The server uses STM memory for persistence,\n\
|
||||
@@ -56,7 +57,7 @@ ntfServerCLIConfig =
|
||||
<> defaultServerPort
|
||||
<> "\n\
|
||||
\websockets: off\n",
|
||||
mkServerConfig = \_storeLogFile transports _ ->
|
||||
mkServerConfig = \storeLogFile transports _ ->
|
||||
NtfServerConfig
|
||||
{ transports,
|
||||
subIdBytes = 24,
|
||||
@@ -67,6 +68,8 @@ ntfServerCLIConfig =
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
apnsConfig = defaultAPNSPushClientConfig,
|
||||
inactiveClientExpiration = Nothing,
|
||||
storeLogFile,
|
||||
resubscribeDelay = 100000, -- 100ms
|
||||
caCertificateFile = caCrtFile,
|
||||
privateKeyFile = serverKeyFile,
|
||||
certificateFile = serverCrtFile
|
||||
|
||||
@@ -13,7 +13,7 @@ import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server.CLI (ServerCLIConfig (..), protocolServerCLI, readStrictIni)
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defaultInactiveClientExpiration, defaultMessageExpiration)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport (simplexMQVersion, supportedSMPServerVRange)
|
||||
import System.FilePath (combine)
|
||||
|
||||
cfgPath :: FilePath
|
||||
@@ -102,6 +102,7 @@ smpServerCLIConfig =
|
||||
}
|
||||
else Nothing,
|
||||
logStatsInterval = Just 86400, -- seconds
|
||||
logStatsStartTime = 0 -- seconds from 00:00 UTC
|
||||
logStatsStartTime = 0, -- seconds from 00:00 UTC
|
||||
smpServerVRange = supportedSMPServerVRange
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 2.3.0
|
||||
version: 3.0.0
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
@@ -48,7 +48,7 @@ dependencies:
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.15.*
|
||||
- mtl == 2.2.*
|
||||
- network == 3.1.2.*
|
||||
- network >= 3.1.2.7 && < 3.2
|
||||
- network-transport == 0.5.*
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- QuickCheck == 2.14.*
|
||||
@@ -60,6 +60,7 @@ dependencies:
|
||||
- template-haskell == 2.16.*
|
||||
- text == 1.2.*
|
||||
- time == 1.9.*
|
||||
- time-compat == 1.9.*
|
||||
- time-manager == 0.0.*
|
||||
- tls >= 1.5.7 && < 1.6
|
||||
- transformers == 0.5.*
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice
|
||||
participant AA as Alice's<br>agent
|
||||
participant AS as Alice's<br>server
|
||||
participant BS as Bob's<br>server
|
||||
participant BA as Bob's<br>agent
|
||||
participant B as Bob
|
||||
|
||||
note over AA, BA: status (receive/send): NONE/NONE
|
||||
|
||||
note over A, AA: 1. request connection<br>from agent
|
||||
A ->> AA: NEW: create<br>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<br>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:<br>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<br>sender server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: CONF: connection request ID<br>and Bob's info
|
||||
A ->> AA: LET: accept connection request,<br>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<br>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<br>about connection success
|
||||
BA ->> B: CON: connected
|
||||
|
||||
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
|
||||
note over AA: status: SECURED/ACTIVE
|
||||
AA ->> AS: ACK: confirm message
|
||||
note over A, AA: 7a. notify Alice<br>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<br>knows Alice can send
|
||||
note over BA: status: ACTIVE/ACTIVE
|
||||
BA ->> BS: ACK: confirm message
|
||||
@@ -33,8 +33,8 @@ sequenceDiagram
|
||||
AS ->> AA: MSG: Bob's info and<br>sender server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: REQ: connection request ID<br>and Bob's info
|
||||
A ->> AA: ACPT: accept connection request,<br>send Alice's info
|
||||
AA ->> A: CONF: connection request ID<br>and Bob's info
|
||||
A ->> AA: LET: accept connection request,<br>send Alice's info
|
||||
AA ->> AS: KEY: secure queue
|
||||
note over AA: status: SECURED/NONE
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
- [Subscribe to queue](#subscribe-to-queue)
|
||||
- [Secure queue command](#secure-queue-command)
|
||||
- [Enable notifications command](#enable-notifications-command)
|
||||
- [Disable notifications command](#disable-notifications-command)
|
||||
- [Acknowledge message delivery](#acknowledge-message-delivery)
|
||||
- [Suspend queue](#suspend-queue)
|
||||
- [Delete queue](#delete-queue)
|
||||
@@ -355,6 +356,7 @@ To protect the privacy of the recipients, there are several commands in SMP prot
|
||||
The clients can optionally instruct a dedicated push notification server to subscribe to notifications and deliver push notifications to the device, which can then retrieve the messages in the background and send local notifications to the user - this is out of scope of SMP protocol. The commands that SMP protocol provides to allow it:
|
||||
|
||||
- `enableNotifications` (`"NKEY"`) with `notifierId` (`"NID"`) response - see [Enable notifications command](#enable-notifications-command).
|
||||
- `disableNotifications` (`"NDEL"`) - see [Disable notifications command](#disable-notifications-command).
|
||||
- `subscribeNotifications` (`"NSUB"`) - see [Subscribe to queue notifications](#subscribe-to-queue-notifications).
|
||||
- `messageNotification` (`"NMSG"`) - see [Deliver message notification](#deliver-message-notification).
|
||||
|
||||
@@ -401,7 +403,7 @@ Commands syntax below is provided using [ABNF][8] with [case-sensitive strings e
|
||||
|
||||
```abnf
|
||||
smpCommand = ping / recipientCmd / send / subscribeNotifications / serverMsg
|
||||
recipientCmd = create / subscribe / secure / enableNotifications /
|
||||
recipientCmd = create / subscribe / secure / enableNotifications / disableNotifications /
|
||||
acknowledge / suspend / delete
|
||||
serverMsg = queueIds / message / notifierId / messageNotification /
|
||||
unsubscribed / ok / error
|
||||
@@ -502,22 +504,42 @@ Once the queue is secured only signed messages can be sent to it.
|
||||
This command is sent by the recipient to the server to add notifier's key to the queue, to allow push notifications server to receive notifications when the message arrives, via a separate queue ID, without receiving message content.
|
||||
|
||||
```abnf
|
||||
enableNotifications = %s"NKEY " notifierKey
|
||||
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
|
||||
|
||||
recipientNotificationDhPublicKey = length x509encoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt notification metadata (encryptedNMsgMeta in NMSG)
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
```
|
||||
|
||||
The server will respond with `notifierId` response if notifications were enabled and the notifier's key was successfully added to the queue:
|
||||
|
||||
```abnf
|
||||
notifierId = %s"NID " notifierId
|
||||
notifierId = %s"NID " notifierId srvNotificationDhPublicKey
|
||||
notifierId = length *OCTET ; 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)
|
||||
```
|
||||
|
||||
This response is sent with the recipient's queue ID (the third part of the transmission).
|
||||
|
||||
To receive the message notifications, `subscribeNotifications` command ("NSUB") must be sent signed with the notifier's key.
|
||||
|
||||
#### Disable notifications command
|
||||
|
||||
This command is sent by the recipient to the server to remove notifier's credentials from the queue:
|
||||
|
||||
```abnf
|
||||
disableNotifications = %s"NDEL"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### 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:
|
||||
@@ -735,10 +757,19 @@ See its syntax in [Enable notifications command](#enable-notifications-command)
|
||||
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"
|
||||
messageNotification = %s"NMSG " nmsgNonce encryptedNMsgMeta
|
||||
|
||||
encryptedNMsgMeta = <encrypted message metadata passed in notification>
|
||||
; metadata E2E encrypted between server and recipient containing server's message ID and timestamp (allows extension),
|
||||
; to be passed to the recipient by the notifier for them to decrypt
|
||||
; with key negotiated in NKEY and NID commands using nmsgNonce
|
||||
|
||||
nmsgNonce = <nonce used in NaCl crypto_box encryption scheme>
|
||||
; nonce used by the server for encryption of message metadata, to be passed to the recipient by the notifier
|
||||
; for them to use in decryption of E2E encrypted metadata
|
||||
```
|
||||
|
||||
Message notification does not contain any message data or meta-data, it only notifies that the message is available.
|
||||
Message notification does not contain any message data or non E2E encrypted metadata.
|
||||
|
||||
#### Subscription END notification
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# DB access and processing messages for iOS notification service extension
|
||||
|
||||
## Problem
|
||||
|
||||
The only way to receive/process notificaitons is via a separate NSE process that requires a concurrent DB and network access.
|
||||
|
||||
SQLite concurenncy does not work, so we need to sync database access.
|
||||
|
||||
The problem is complex, as we do not directly control db access from the app, it can be triggered by the message arriving and it may fail to complete in case the app is suspended in the background.
|
||||
|
||||
So we need to prevent db access from starting when we know the app is about to be suspended.
|
||||
|
||||
The last problem is how to receive and process messages in NSE - should it use recently added GET command or should it subscribe to the connections that receive messages and process messages normally.
|
||||
|
||||
To summarize, 2 problems need to be solved:
|
||||
|
||||
1. sync db access between 2 (or more, if we add share extension) processes
|
||||
|
||||
2. prevent access from starting when the process is due to suspend, only complete operations.
|
||||
|
||||
3. Receiveing and processing agent messages in NSE
|
||||
|
||||
## Proposed solution
|
||||
|
||||
For problem 1, we can use Posix semaphores from our core code, in the same bracket that manages database connection - it would wait for semaphore to be free and unlock it once the db operation is complete.
|
||||
|
||||
For problem 2, we need to communicate from the app when it goes to the background to prevent database access starting and completing before the suspension. This would set some `aboutToSuspend` STM flag (or the opposite) that would prevent operations from progressing (using STM retry, that would block until the flag has the value allowing operation to progress).
|
||||
|
||||
Several possibilities can be considered:
|
||||
|
||||
- use this flag in the bracket that provides DB connections. While simple, it may lock some operations in the middle and may also lead to the situation when network operation succeeds but database access was blocked, and the database is not updated.
|
||||
- use this flag to stop network operations that would require database updates - like sending messages, subscriptions and ACK - all these operations would require database access once they succeed.
|
||||
- use two flags, for both cases above, but set them at different times since going to background - block new network operations as soon as the app goes to the background and block database access once the app is about to be suspended.
|
||||
|
||||
The last option seems more robust. To do it, there will be an api allowing the app to communicat its phase:
|
||||
|
||||
- app going to the background would trigger blocking new network operations and start a new background task - `background` phase.
|
||||
- background task receiving completion warning, or, maybe, some time after it is started - probably 20 seconds - or whatever happens earlier - would trigger call blocking db access - `suspended` phase.
|
||||
- app becoming active would trigger unblocking both flags - `active` phase.
|
||||
|
||||
`/_app phase <phase>` where `phase` can be one of the above values.
|
||||
|
||||
NSE would also use the same phases:
|
||||
|
||||
- sending `active` when it is started (the process starts as active, but it is possible that the new notification arrives to the same process, after the previous one sent background/suspension)
|
||||
- sending `background + suspended` (or `suspended` should set both flags) once it is finished processing the notification, provided no new notification arrived and started processing - this should be tracked in NSE.
|
||||
|
||||
For problem 3, NSE can do one of the following:
|
||||
|
||||
- use SUB and process messages normally - the downside is that the app will have to resubscribe and it has to be tracked.
|
||||
- use GET and process messages by pushing them through the processing function - the downside it that a rewiring of message processing is needed.
|
||||
- use GET but deliver messages through the same queue as when they arrive normally (in which case getMessage agent function should not return the message, but will return a flag showing whether the message was received, or, possibly, or, possibly will return a message but the message would also be sent to the queue?).
|
||||
- process messages in agent manually and in chat via the queue.
|
||||
|
||||
One of the downside of GET is that it requires calling GET again after ACK. We could have two variants of ACK (or additional ACK parameter) - one that never delivers a new message, and another one that does. In this case, if get needs to process the next message (when the current one has no notification flag), it can call ACK that delivers the next message. But, it is probably a premature optimization, and having general support of batched commands would add more value.
|
||||
|
||||
Additional problem is concurrency in NSE - if the new notification from the same queue arrives before the current one finishes processing in the same process one of the following can happen:
|
||||
|
||||
- the 2nd notification naively call GET and receives the same message.
|
||||
- the 2nd notification waits until the first finished processing, in which case it can run out of time.
|
||||
|
||||
The problem is that the app won't know it's the same queue, as nId is encrypted, so the agent should handle this scenario when the new call to getMessage is made before the previous one finished processing, and differentiate between calls made for additional messages (possibly, getMessage should include previous message ID, if it is available) and the first call.
|
||||
|
||||
EDIT: GETs have to be sent from UI to chat and from chat to agent as function calls, but the agent will have to queue get calls to make sure they return different messages. GET call would return message flags (incl. notification flag), so that the UI can send the next GET if needed without waiting.
|
||||
|
||||
Considered alternative: include notification content in the message and have NSE only perform decryption, without any network IO. In this case notification content would be in SEND and in NMSG, e2e encrypted.
|
||||
|
||||
While promising, as it solves network coordination issues and makes GET unnecessary, it creates mutliple other problems, so it was rejected:
|
||||
|
||||
- message content is exposed to centralized ntf and apns servers, creating additional attack vector.
|
||||
- it adds complexity in security critical parts of the stack - double ratchet encryption, as it requires either storing message keys and using different IVs for notifications, or initializing completely separate ratchet for notifications content.
|
||||
- it reduces the size of the message.
|
||||
- it makes user experience worse, as:
|
||||
- it would not accelerate handshake for new contacts and for file delivery - this approach only works for content messages.
|
||||
- it would open the app without the new messages - the users would have to wait until the messages are received. It is also bad for "security optics" - the users might think that the message content was exposed to notifications.
|
||||
@@ -0,0 +1,61 @@
|
||||
sequenceDiagram
|
||||
participant M as iOS message<br>notification
|
||||
participant S as iOS system
|
||||
participant N as iOS NSE
|
||||
participant U as iOS UI
|
||||
participant C as Core chat
|
||||
participant A as Core agent
|
||||
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref
|
||||
note over N: ignore,<br>app is active
|
||||
|
||||
note over M, A: app going to background
|
||||
S ->> U: phase: background<br>(possibly, "will" method)
|
||||
U ->> S: set app pref "pausing"
|
||||
U ->> C: /_app phase paused, result CRCmdOk
|
||||
C ->> A: pauseAgent<br>(no new network IO)
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref
|
||||
note over N: wait/poll for<br>"paused"/"suspending"/"suspended"<br>event/pref
|
||||
A ->> C: event "IO paused"<br>(after in-flight op completed)<br>PHASE PAUSED
|
||||
C ->> U: event "IO paused" (CRAppPaused)
|
||||
U ->> S: set shared pref "paused"
|
||||
|
||||
note over M, A: process notification
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref<br>continue if<br>"paused"/"suspending"/"suspended"
|
||||
N ->> S: set NSE pref "active"
|
||||
N ->> C: /_get message
|
||||
C ->> A: getMessage
|
||||
A ->> C: msg flags
|
||||
C ->> N: msg flags
|
||||
note over N: get messages<br>until notification flag set
|
||||
A ->> C: MSG/CONF/INFO
|
||||
C ->> N: some event
|
||||
N ->> S: set NSE pref "completed"
|
||||
N ->> S: show notification
|
||||
|
||||
note over M, A: app about to be suspended<br>(or 15-20 sec after background)
|
||||
S ->> U: background task notice
|
||||
U ->> S: set app pref "suspending"
|
||||
U ->> C: /_app phase suspended, response ok
|
||||
C ->> A: suspendAgent<br>(no new DB)
|
||||
A ->> C: event "DB paused"<br>(after in-flight op completed)<br>PHASE SUSPENDED
|
||||
C ->> U: event "DB paused" (CRAppSuspended)
|
||||
U ->> S: set app pref "suspended"
|
||||
|
||||
note over M, A: app about to be activated
|
||||
S ->> U: phase: active<br>(or inactive?)<br><br>(possibly, "will" method)
|
||||
S ->> U: get NSE pref
|
||||
U ->> S: set app pref "activating"
|
||||
alt nse active?
|
||||
U ->> C: /_app phase inactive
|
||||
note over U: poll/wait till NSE pref is "completed"
|
||||
end
|
||||
|
||||
U ->> C: /_app phase active (response result)
|
||||
C ->> A: activateAgent<br>(allow IO/DB)
|
||||
A ->> C: result ()
|
||||
C ->> U: CRCmdOk
|
||||
U ->> S: set app pref "active"
|
||||
@@ -0,0 +1,34 @@
|
||||
# SMP protocol changes to support push notifications on iOS
|
||||
|
||||
## Problem
|
||||
|
||||
There are already commands/responses to allow subscriptions to message notifications - NKEY/NID, NSUB/NMSG. These commands will be used by SMP agent (NKEY/NID) and by notification server (NSUB/NMSG) to have message notifications delivered to notification server, so it can forward them to APNS server using device token.
|
||||
|
||||
There are two remaining problems that these commands do not solve.
|
||||
|
||||
1. Receiving the message when notification arrives.
|
||||
|
||||
iOS requires creating a bundled notification service extension (NSE) that runs in isolated container and, if we were to use the existing commands, would have SMP subscription to the same SMP servers as the main app, triggering resubscriptions every time the message reception switches between the app and NSE. That would cause a substantial increase in traffic and battery consumption to the users.
|
||||
|
||||
2. Showing notifications for service messages.
|
||||
|
||||
Users do not expect to see notifications for every single SMP messages - e.g., we currently do not show notifications when messages are edited and deleted, and users do not expect them. NSE requires that for every received push notification there should be some notification shown to the users. So only we would have to show a notification for message deletes and updates, we would have to show it for all service messages - e.g. user accepted file invitation, or file transmission has started, contact profile updates and so on.
|
||||
|
||||
We considered differentiating whether notifications are sent per queue, from the recipient side, so we do not send notifications for file queues. But it seems insufficient, particularly if we add such features as message receipts, status updates, etc.
|
||||
|
||||
## Proposal
|
||||
|
||||
1. To retrieve messages when push notifications arrive, we will add 2 SMP commands:
|
||||
|
||||
- GET: retrieve one message from the connection. Resonse could be either MSG (the same as when MSG is delivered, but with the correlation id) or GMSG (to simplify processing) – TBC during implementation. If message is not available, the response could be ERR NO_MSG
|
||||
- ACK or GACK: acknowledge that the message was processed by the client and can be removed - TBC which one is better. The response is OK or ERR NO_MSG if there was nothing to acknowledge (same as with ACK now)
|
||||
|
||||
This would allow receiving a single message from the queue without subscription, this way avoiding that the main app is unsubscribed from the queue.
|
||||
|
||||
2. The only way to avoid showing unnecessary notifications (status updates, service messages, etc.) is to avoid sending them. That requires instructing SMP server whether notification should be sent both per queue, from the recipient side, and per message - from the sender side. So the notification would only be sent if the queue has them enabled (via NKEY command) and the sender includes an additional flag in SEND command. The same flag should be included into MSG, so when the message is retrieved with GET command, the client knows, on the agent or chat level (or both), whether this message should have notification shown to the user, and if not - retrieve the next one(s) as well.
|
||||
|
||||
This is a substantial change to SMP protocol, that would require client and server upgrade for notifications to be supported.
|
||||
|
||||
We should consider whether to increase the SMP protocol version number to 2, so that the new clients can connect to the old clients but without notifications, or we could keep the old commands in the protocol and instead of adding flags to the existing commands, create new commands.
|
||||
|
||||
We can also consider making commands extensible so that the new flags can be added (and ignored by parsers if not supported) to at least some existing commands.
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
|
||||
# <UDF name="api_token" label="Linode API token - enable Linode to create tags with server address, fingerprint and version. Note: minimal permissions token should have are read/write access to `linodes` (to create tags) and `domains` (to add A record for the third level domain if FQDN is provided)." default="" />
|
||||
# TODO review
|
||||
# <UDF name="fqdn" label="FQDN (Fully Qualified Domain Name) - provide third level domain name (e.g. smp.example.com). If provided use `smp://fingerprint@FQDN` as server address in the client. If FQDN is not provided use `smp://fingerprint@IP` instead." default="" />
|
||||
# <UDF name="apns_key_id" label="APNS key ID." default="" />
|
||||
|
||||
# Log all stdout output to stackscript.log
|
||||
exec &> >(tee -i /var/log/stackscript.log)
|
||||
|
||||
# Uncomment next line to enable debugging features
|
||||
# set -xeo pipefail
|
||||
|
||||
cd $HOME
|
||||
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
install jq
|
||||
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
|
||||
# Increase file descriptors limit
|
||||
echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'fs.inode-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'root soft nofile unlimited' >> /etc/security/limits.conf
|
||||
echo 'root hard nofile unlimited' >> /etc/security/limits.conf
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex-notifications/bin"
|
||||
binary="$bin_dir/ntf-server"
|
||||
mkdir -p $bin_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/ntf-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
ntf-server --version
|
||||
|
||||
# Initialize server
|
||||
init_opts=()
|
||||
|
||||
ip_address=$(curl ifconfig.me)
|
||||
init_opts+=(--ip $ip_address)
|
||||
|
||||
[[ -n "$FQDN" ]] && init_opts+=(-n $FQDN)
|
||||
|
||||
ntf-server init "${init_opts[@]}"
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex-notifications/fingerprint)
|
||||
|
||||
# Determine server address to specify in welcome script and Linode tag
|
||||
# ! If FQDN was provided and used as part of server initialization, server's certificate will not pass validation at client
|
||||
# ! if client tries to connect by server's IP address, so we have to specify FQDN as server address in Linode tag and
|
||||
# ! in welcome script regardless of creation of A record in Linode
|
||||
# ! https://hackage.haskell.org/package/x509-validation-1.6.10/docs/src/Data-X509-Validation.html#validateCertificateName
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
server_address=$FQDN
|
||||
else
|
||||
server_address=$ip_address
|
||||
fi
|
||||
|
||||
# Set up welcome script
|
||||
on_login_script="/opt/simplex-notifications/on_login.sh"
|
||||
|
||||
# TODO fix address
|
||||
# / Welcome script
|
||||
cat > $on_login_script << EOF
|
||||
#!/bin/bash
|
||||
|
||||
fingerprint=\$1
|
||||
server_address=\$2
|
||||
|
||||
cat << EOF2
|
||||
********************************************************************************
|
||||
|
||||
SimpleX notifications server address: smp://\$fingerprint@\$server_address
|
||||
Check server status with: systemctl status ntf-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (notifications server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex-notifications/on_login.sh - from /root/.bashrc
|
||||
EOF2
|
||||
|
||||
EOF
|
||||
# Welcome script /
|
||||
|
||||
chmod +x $on_login_script
|
||||
echo "bash $on_login_script $fingerprint $server_address" >> /root/.bashrc
|
||||
|
||||
# Create A record and update Linode's tags
|
||||
if [[ -n "$API_TOKEN" ]]; then
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ -n $domain_id ]]; then
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
fi
|
||||
fi
|
||||
|
||||
version=$(ntf-server --version | cut -d ' ' -f 3-)
|
||||
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$server_address\",\"$fingerprint\",\"$version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
fi
|
||||
|
||||
# / Create systemd service
|
||||
cat > /etc/systemd/system/ntf-server.service << EOF
|
||||
[Unit]
|
||||
Description=SimpleX notifications server
|
||||
|
||||
[Service]
|
||||
Environment="APNS_KEY_FILE=/etc/opt/simplex-notifications/AuthKey.p8"
|
||||
Environment="APNS_KEY_ID=$APNS_KEY_ID"
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex-notifications/ntf-server.log 2>&1"
|
||||
KillSignal=SIGINT
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=1000000
|
||||
LimitNOFILESoft=1000000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service /
|
||||
|
||||
# Start systemd service
|
||||
chmod 644 /etc/systemd/system/ntf-server.service
|
||||
sudo systemctl enable ntf-server
|
||||
# ! APNS key file and certificate have to be copied manually
|
||||
# sudo systemctl start ntf-server
|
||||
|
||||
# Reboot Linode to apply upgrades
|
||||
# sudo reboot
|
||||
@@ -158,6 +158,7 @@ Description=SMP server
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=1000000
|
||||
|
||||
+16
-7
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 2.3.0
|
||||
version: 3.0.0
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -37,6 +37,7 @@ library
|
||||
Simplex.Messaging.Agent
|
||||
Simplex.Messaging.Agent.Client
|
||||
Simplex.Messaging.Agent.Env.SQLite
|
||||
Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
Simplex.Messaging.Agent.Protocol
|
||||
Simplex.Messaging.Agent.QueryString
|
||||
Simplex.Messaging.Agent.RetryInterval
|
||||
@@ -47,7 +48,8 @@ library
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220404_ntf_subscriptions_draft
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
Simplex.Messaging.Crypto
|
||||
@@ -60,7 +62,9 @@ library
|
||||
Simplex.Messaging.Notifications.Server.Env
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
Simplex.Messaging.Notifications.Server.Store
|
||||
Simplex.Messaging.Notifications.Server.StoreLog
|
||||
Simplex.Messaging.Notifications.Transport
|
||||
Simplex.Messaging.Notifications.Types
|
||||
Simplex.Messaging.Parsers
|
||||
Simplex.Messaging.Protocol
|
||||
Simplex.Messaging.Server
|
||||
@@ -116,7 +120,7 @@ library
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
@@ -127,6 +131,7 @@ library
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.5.7 && <1.6
|
||||
, transformers ==0.5.*
|
||||
@@ -175,7 +180,7 @@ executable ntf-server
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
@@ -187,6 +192,7 @@ executable ntf-server
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.5.7 && <1.6
|
||||
, transformers ==0.5.*
|
||||
@@ -235,7 +241,7 @@ executable smp-agent
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
@@ -247,6 +253,7 @@ executable smp-agent
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.5.7 && <1.6
|
||||
, transformers ==0.5.*
|
||||
@@ -295,7 +302,7 @@ executable smp-server
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
@@ -307,6 +314,7 @@ executable smp-server
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.5.7 && <1.6
|
||||
, transformers ==0.5.*
|
||||
@@ -374,7 +382,7 @@ test-suite smp-server-test
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
@@ -386,6 +394,7 @@ test-suite smp-server-test
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, timeit ==2.0.*
|
||||
, tls >=1.5.7 && <1.6
|
||||
|
||||
+558
-324
File diff suppressed because it is too large
Load Diff
@@ -4,32 +4,41 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Client
|
||||
( AgentClient (..),
|
||||
newAgentClient,
|
||||
AgentMonad,
|
||||
withAgentLock,
|
||||
closeAgentClient,
|
||||
newRcvQueue,
|
||||
subscribeQueue,
|
||||
getQueueMessage,
|
||||
addSubscription,
|
||||
getSubscriptions,
|
||||
sendConfirmation,
|
||||
sendInvitation,
|
||||
RetryInterval (..),
|
||||
secureQueue,
|
||||
enableQueueNotifications,
|
||||
disableQueueNotifications,
|
||||
sendAgentMessage,
|
||||
agentNtfRegisterToken,
|
||||
agentNtfVerifyToken,
|
||||
agentNtfCheckToken,
|
||||
agentNtfReplaceToken,
|
||||
agentNtfDeleteToken,
|
||||
agentNtfEnableCron,
|
||||
agentNtfCreateSubscription,
|
||||
agentNtfCheckSubscription,
|
||||
agentNtfDeleteSubscription,
|
||||
agentCbEncrypt,
|
||||
agentCbDecrypt,
|
||||
cryptoError,
|
||||
@@ -40,12 +49,23 @@ module Simplex.Messaging.Agent.Client
|
||||
removeSubscription,
|
||||
hasActiveSubscription,
|
||||
agentDbPath,
|
||||
AgentOperation (..),
|
||||
AgentOpState (..),
|
||||
AgentState (..),
|
||||
beginAgentOperation,
|
||||
endAgentOperation,
|
||||
suspendSendingAndDatabase,
|
||||
suspendOperation,
|
||||
notifySuspended,
|
||||
whenSuspending,
|
||||
withStore,
|
||||
withStore',
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Concurrent.Async (Async, uninterruptibleCancel)
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Concurrent.STM (retry, stateTVar)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
@@ -58,20 +78,24 @@ import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (catMaybes)
|
||||
import Data.Set (Set)
|
||||
import Data.Text.Encoding
|
||||
import Data.Word (Word16)
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
-- import GHC.Conc (unsafeIOToSTM)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), withTransaction)
|
||||
import Simplex.Messaging.Client
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Notifications.Client
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, ErrorType, ProtocolServer (..), QueueId, QueueIdsKeys (..), SndPublicVerifyKey)
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, ErrorType, MsgFlags (..), MsgId, NotifierId, NtfPrivateSignKey, NtfPublicVerifyKey, ProtocolServer (..), QueueId, QueueIdsKeys (..), RcvNtfPublicDhKey, SMPMsgMeta, SndPublicVerifyKey)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -94,8 +118,8 @@ data AgentClient = AgentClient
|
||||
subQ :: TBQueue (ATransmission 'Agent),
|
||||
msgQ :: TBQueue (ServerTransmission BrokerMsg),
|
||||
smpServers :: TVar (NonEmpty SMPServer),
|
||||
ntfServers :: TVar [NtfServer],
|
||||
smpClients :: TMap SMPServer SMPClientVar,
|
||||
ntfServers :: TVar [NtfServer],
|
||||
ntfClients :: TMap NtfServer NtfClientVar,
|
||||
subscrSrvrs :: TMap SMPServer (TMap ConnId RcvQueue),
|
||||
pendingSubscrSrvrs :: TMap SMPServer (TMap ConnId RcvQueue),
|
||||
@@ -103,14 +127,34 @@ data AgentClient = AgentClient
|
||||
connMsgsQueued :: TMap ConnId Bool,
|
||||
smpQueueMsgQueues :: TMap (ConnId, SMPServer, SMP.SenderId) (TQueue InternalId),
|
||||
smpQueueMsgDeliveries :: TMap (ConnId, SMPServer, SMP.SenderId) (Async ()),
|
||||
rcvNetworkOp :: TVar AgentOpState,
|
||||
msgDeliveryOp :: TVar AgentOpState,
|
||||
sndNetworkOp :: TVar AgentOpState,
|
||||
databaseOp :: TVar AgentOpState,
|
||||
agentState :: TVar AgentState,
|
||||
getMsgLocks :: TMap (SMPServer, SMP.RecipientId) (TMVar ()),
|
||||
reconnections :: TVar [Async ()],
|
||||
asyncClients :: TVar [Async ()],
|
||||
clientId :: Int,
|
||||
agentEnv :: Env,
|
||||
smpSubscriber :: Async (),
|
||||
lock :: TMVar ()
|
||||
}
|
||||
|
||||
data AgentOperation = AORcvNetwork | AOMsgDelivery | AOSndNetwork | AODatabase
|
||||
deriving (Eq, Show)
|
||||
|
||||
agentOpSel :: AgentOperation -> (AgentClient -> TVar AgentOpState)
|
||||
agentOpSel = \case
|
||||
AORcvNetwork -> rcvNetworkOp
|
||||
AOMsgDelivery -> msgDeliveryOp
|
||||
AOSndNetwork -> sndNetworkOp
|
||||
AODatabase -> databaseOp
|
||||
|
||||
data AgentOpState = AgentOpState {opSuspended :: Bool, opsInProgress :: Int}
|
||||
|
||||
data AgentState = ASActive | ASSuspending | ASSuspended
|
||||
deriving (Eq, Show)
|
||||
|
||||
newAgentClient :: InitialAgentServers -> Env -> STM AgentClient
|
||||
newAgentClient InitialAgentServers {smp, ntf} agentEnv = do
|
||||
let qSize = tbqSize $ config agentEnv
|
||||
@@ -119,8 +163,8 @@ newAgentClient InitialAgentServers {smp, ntf} agentEnv = do
|
||||
subQ <- newTBQueue qSize
|
||||
msgQ <- newTBQueue qSize
|
||||
smpServers <- newTVar smp
|
||||
ntfServers <- newTVar ntf
|
||||
smpClients <- TM.empty
|
||||
ntfServers <- newTVar ntf
|
||||
ntfClients <- TM.empty
|
||||
subscrSrvrs <- TM.empty
|
||||
pendingSubscrSrvrs <- TM.empty
|
||||
@@ -128,29 +172,32 @@ newAgentClient InitialAgentServers {smp, ntf} agentEnv = do
|
||||
connMsgsQueued <- TM.empty
|
||||
smpQueueMsgQueues <- TM.empty
|
||||
smpQueueMsgDeliveries <- TM.empty
|
||||
rcvNetworkOp <- newTVar $ AgentOpState False 0
|
||||
msgDeliveryOp <- newTVar $ AgentOpState False 0
|
||||
sndNetworkOp <- newTVar $ AgentOpState False 0
|
||||
databaseOp <- newTVar $ AgentOpState False 0
|
||||
agentState <- newTVar ASActive
|
||||
getMsgLocks <- TM.empty
|
||||
reconnections <- newTVar []
|
||||
asyncClients <- newTVar []
|
||||
clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1)
|
||||
clientId <- stateTVar (clientCounter agentEnv) $ \i -> let i' = i + 1 in (i', i')
|
||||
lock <- newTMVar ()
|
||||
return AgentClient {active, rcvQ, subQ, msgQ, smpServers, ntfServers, smpClients, ntfClients, subscrSrvrs, pendingSubscrSrvrs, subscrConns, connMsgsQueued, smpQueueMsgQueues, smpQueueMsgDeliveries, reconnections, asyncClients, clientId, agentEnv, smpSubscriber = undefined, lock}
|
||||
return AgentClient {active, rcvQ, subQ, msgQ, smpServers, smpClients, ntfServers, ntfClients, subscrSrvrs, pendingSubscrSrvrs, subscrConns, connMsgsQueued, smpQueueMsgQueues, smpQueueMsgDeliveries, rcvNetworkOp, msgDeliveryOp, sndNetworkOp, databaseOp, agentState, getMsgLocks, reconnections, asyncClients, clientId, agentEnv, lock}
|
||||
|
||||
agentDbPath :: AgentClient -> FilePath
|
||||
agentDbPath AgentClient {agentEnv = Env {store = SQLiteStore {dbFilePath}}} = dbFilePath
|
||||
|
||||
-- | Agent monad with MonadReader Env and MonadError AgentErrorType
|
||||
type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m)
|
||||
|
||||
class ProtocolServerClient msg where
|
||||
getProtocolServerClient :: AgentMonad m => AgentClient -> ProtocolServer -> m (ProtocolClient msg)
|
||||
protocolError :: ErrorType -> AgentErrorType
|
||||
clientProtocolError :: ErrorType -> AgentErrorType
|
||||
|
||||
instance ProtocolServerClient BrokerMsg where
|
||||
getProtocolServerClient = getSMPServerClient
|
||||
protocolError = SMP
|
||||
clientProtocolError = SMP
|
||||
|
||||
instance ProtocolServerClient NtfResponse where
|
||||
getProtocolServerClient = getNtfServerClient
|
||||
protocolError = NTF
|
||||
clientProtocolError = NTF
|
||||
|
||||
getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient
|
||||
getSMPServerClient c@AgentClient {active, smpClients, msgQ} srv = do
|
||||
@@ -193,6 +240,7 @@ getSMPServerClient c@AgentClient {active, smpClients, msgQ} srv = do
|
||||
whenM (readTVarIO active) $ do
|
||||
let conns = M.keys cs
|
||||
unless (null conns) . notifySub "" $ DOWN srv conns
|
||||
atomically $ mapM_ (releaseGetLock c) cs
|
||||
unliftIO u reconnectServer
|
||||
|
||||
reconnectServer :: m ()
|
||||
@@ -211,6 +259,9 @@ getSMPServerClient c@AgentClient {active, smpClients, msgQ} srv = do
|
||||
n <- asks $ resubscriptionConcurrency . config
|
||||
withAgentLock c . withClient c srv $ \smp -> do
|
||||
cs <- atomically $ mapM readTVar =<< TM.lookup srv (pendingSubscrSrvrs c)
|
||||
-- TODO if any of the subscriptions fails here (e.g. because of timeout), it terminates the whole process for all subscriptions
|
||||
-- instead it should only report successful subscriptions and schedule the next call to reconnectClient to subscribe for the remaining subscriptions
|
||||
-- this way, for each DOWN event there can be several UP events
|
||||
conns <- pooledForConcurrentlyN n (maybe [] M.toList cs) $ \sub@(connId, _) ->
|
||||
ifM
|
||||
(atomically $ hasActiveSubscription c connId)
|
||||
@@ -320,8 +371,10 @@ closeAgentClient c = liftIO $ do
|
||||
clear subscrConns
|
||||
clear connMsgsQueued
|
||||
clear smpQueueMsgQueues
|
||||
clear getMsgLocks
|
||||
where
|
||||
clientTimeout sel = tcpTimeout . sel . config $ agentEnv c
|
||||
clear :: (AgentClient -> TMap k a) -> IO ()
|
||||
clear sel = atomically $ writeTVar (sel c) M.empty
|
||||
|
||||
closeProtocolServerClients :: Int -> TMap ProtocolServer (ClientVar msg) -> IO ()
|
||||
@@ -357,10 +410,10 @@ withLogClient_ c srv qId cmdStr action = do
|
||||
return res
|
||||
|
||||
withClient :: forall m msg a. (AgentMonad m, ProtocolServerClient msg) => AgentClient -> ProtocolServer -> (ProtocolClient msg -> ExceptT ProtocolClientError IO a) -> m a
|
||||
withClient c srv action = withClient_ c srv $ liftClient (protocolError @msg) . action
|
||||
withClient c srv action = withClient_ c srv $ liftClient (clientProtocolError @msg) . action
|
||||
|
||||
withLogClient :: forall m msg a. (AgentMonad m, ProtocolServerClient msg) => AgentClient -> ProtocolServer -> QueueId -> ByteString -> (ProtocolClient msg -> ExceptT ProtocolClientError IO a) -> m a
|
||||
withLogClient c srv qId cmdStr action = withLogClient_ c srv qId cmdStr $ liftClient (protocolError @msg) . action
|
||||
withLogClient c srv qId cmdStr action = withLogClient_ c srv qId cmdStr $ liftClient (clientProtocolError @msg) . action
|
||||
|
||||
liftClient :: AgentMonad m => (ErrorType -> AgentErrorType) -> ExceptT ProtocolClientError IO a -> m a
|
||||
liftClient = liftError . protocolClientError
|
||||
@@ -369,7 +422,7 @@ protocolClientError :: (ErrorType -> AgentErrorType) -> ProtocolClientError -> A
|
||||
protocolClientError protocolError_ = \case
|
||||
PCEProtocolError e -> protocolError_ e
|
||||
PCEResponseError e -> BROKER $ RESPONSE e
|
||||
PCEUnexpectedResponse -> BROKER UNEXPECTED
|
||||
PCEUnexpectedResponse _ -> BROKER UNEXPECTED
|
||||
PCEResponseTimeout -> BROKER TIMEOUT
|
||||
PCENetworkError -> BROKER NETWORK
|
||||
PCETransportError e -> BROKER $ TRANSPORT e
|
||||
@@ -404,12 +457,14 @@ newRcvQueue_ a c srv = do
|
||||
e2ePrivKey,
|
||||
e2eDhSecret = Nothing,
|
||||
sndId = Just sndId,
|
||||
status = New
|
||||
status = New,
|
||||
clientNtfCreds = Nothing
|
||||
}
|
||||
pure (rq, SMPQueueUri srv sndId SMP.smpClientVRange e2eDhKey)
|
||||
|
||||
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m ()
|
||||
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do
|
||||
whenM (atomically . TM.member (server, rcvId) $ getMsgLocks c) . throwError $ CMD PROHIBITED
|
||||
atomically $ addPendingSubscription c rq connId
|
||||
withLogClient c server rcvId "SUB" $ \smp -> do
|
||||
liftIO (runExceptT $ subscribeSMPQueue smp rcvPrivateKey rcvId) >>= \case
|
||||
@@ -417,7 +472,8 @@ subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do
|
||||
atomically . when (e /= PCENetworkError && e /= PCEResponseTimeout) $
|
||||
removePendingSubscription c server connId
|
||||
throwError e
|
||||
Right _ -> addSubscription c rq connId
|
||||
Right _ -> do
|
||||
addSubscription c rq connId
|
||||
|
||||
addSubscription :: MonadIO m => AgentClient -> RcvQueue -> ConnId -> m ()
|
||||
addSubscription c rq@RcvQueue {server} connId = atomically $ do
|
||||
@@ -449,6 +505,11 @@ removeSubs_ :: TMap SMPServer (TMap ConnId RcvQueue) -> SMPServer -> ConnId -> S
|
||||
removeSubs_ ss server connId =
|
||||
TM.lookup server ss >>= mapM_ (TM.delete connId)
|
||||
|
||||
getSubscriptions :: AgentClient -> STM (Set ConnId)
|
||||
getSubscriptions AgentClient {subscrConns} = do
|
||||
m <- readTVar subscrConns
|
||||
pure $ M.keysSet m
|
||||
|
||||
logServer :: MonadIO m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m ()
|
||||
logServer dir AgentClient {clientId} srv qId cmdStr =
|
||||
logInfo . decodeUtf8 $ B.unwords ["A", "(" <> bshow clientId <> ")", dir, showServer srv, ":", logSecret qId, cmdStr]
|
||||
@@ -465,31 +526,59 @@ sendConfirmation c sq@SndQueue {server, sndId, sndPublicKey = Just sndPublicKey,
|
||||
withLogClient_ c server sndId "SEND <CONF>" $ \smp -> do
|
||||
let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation
|
||||
msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg
|
||||
liftClient SMP $ sendSMPMessage smp Nothing sndId msg
|
||||
liftClient SMP $ sendSMPMessage smp Nothing sndId (SMP.MsgFlags {notification = True}) msg
|
||||
sendConfirmation _ _ _ = throwError $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database"
|
||||
|
||||
sendInvitation :: forall m. AgentMonad m => AgentClient -> Compatible SMPQueueInfo -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> m ()
|
||||
sendInvitation c (Compatible SMPQueueInfo {smpServer, senderId, dhPublicKey}) connReq connInfo =
|
||||
sendInvitation :: forall m. AgentMonad m => AgentClient -> Compatible SMPQueueInfo -> Compatible Version -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> m ()
|
||||
sendInvitation c (Compatible SMPQueueInfo {smpServer, senderId, dhPublicKey}) (Compatible agentVersion) connReq connInfo =
|
||||
withLogClient_ c smpServer senderId "SEND <INV>" $ \smp -> do
|
||||
msg <- mkInvitation
|
||||
liftClient SMP $ sendSMPMessage smp Nothing senderId msg
|
||||
liftClient SMP $ sendSMPMessage smp Nothing senderId MsgFlags {notification = True} msg
|
||||
where
|
||||
mkInvitation :: m ByteString
|
||||
-- this is only encrypted with per-queue E2E, not with double ratchet
|
||||
mkInvitation = do
|
||||
let agentEnvelope = AgentInvitation {agentVersion = smpAgentVersion, connReq, connInfo}
|
||||
let agentEnvelope = AgentInvitation {agentVersion, connReq, connInfo}
|
||||
agentCbEncryptOnce dhPublicKey . smpEncode $
|
||||
SMP.ClientMessage SMP.PHEmpty $ smpEncode agentEnvelope
|
||||
|
||||
getQueueMessage :: AgentMonad m => AgentClient -> RcvQueue -> m (Maybe SMPMsgMeta)
|
||||
getQueueMessage c RcvQueue {server, rcvId, rcvPrivateKey} = do
|
||||
atomically createTakeGetLock
|
||||
withLogClient c server rcvId "GET" $ \smp ->
|
||||
getSMPMessage smp rcvPrivateKey rcvId
|
||||
where
|
||||
createTakeGetLock = TM.alterF takeLock (server, rcvId) $ getMsgLocks c
|
||||
where
|
||||
takeLock l_ = do
|
||||
l <- maybe (newTMVar ()) pure l_
|
||||
takeTMVar l
|
||||
pure $ Just l
|
||||
|
||||
secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SndPublicVerifyKey -> m ()
|
||||
secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey =
|
||||
withLogClient c server rcvId "KEY <key>" $ \smp ->
|
||||
secureSMPQueue smp rcvPrivateKey rcvId senderKey
|
||||
|
||||
sendAck :: AgentMonad m => AgentClient -> RcvQueue -> m ()
|
||||
sendAck c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
enableQueueNotifications :: AgentMonad m => AgentClient -> RcvQueue -> NtfPublicVerifyKey -> RcvNtfPublicDhKey -> m (NotifierId, RcvNtfPublicDhKey)
|
||||
enableQueueNotifications c RcvQueue {server, rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey =
|
||||
withLogClient c server rcvId "NKEY <nkey>" $ \smp ->
|
||||
enableSMPQueueNotifications smp rcvPrivateKey rcvId notifierKey rcvNtfPublicDhKey
|
||||
|
||||
disableQueueNotifications :: AgentMonad m => AgentClient -> RcvQueue -> m ()
|
||||
disableQueueNotifications c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
withLogClient c server rcvId "NDEL" $ \smp ->
|
||||
disableSMPQueueNotifications smp rcvPrivateKey rcvId
|
||||
|
||||
sendAck :: AgentMonad m => AgentClient -> RcvQueue -> MsgId -> m ()
|
||||
sendAck c rq@RcvQueue {server, rcvId, rcvPrivateKey} msgId = do
|
||||
withLogClient c server rcvId "ACK" $ \smp ->
|
||||
ackSMPMessage smp rcvPrivateKey rcvId
|
||||
ackSMPMessage smp rcvPrivateKey rcvId msgId
|
||||
atomically $ releaseGetLock c rq
|
||||
|
||||
releaseGetLock :: AgentClient -> RcvQueue -> STM ()
|
||||
releaseGetLock c RcvQueue {server, rcvId} =
|
||||
TM.lookup (server, rcvId) (getMsgLocks c) >>= mapM_ (void . (`tryPutTMVar` ()))
|
||||
|
||||
suspendQueue :: AgentMonad m => AgentClient -> RcvQueue -> m ()
|
||||
suspendQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
@@ -501,12 +590,12 @@ deleteQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
withLogClient c server rcvId "DEL" $ \smp ->
|
||||
deleteSMPQueue smp rcvPrivateKey rcvId
|
||||
|
||||
sendAgentMessage :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
|
||||
sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} agentMsg =
|
||||
sendAgentMessage :: forall m. AgentMonad m => AgentClient -> SndQueue -> MsgFlags -> ByteString -> m ()
|
||||
sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} msgFlags agentMsg =
|
||||
withLogClient_ c server sndId "SEND <MSG>" $ \smp -> do
|
||||
let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg
|
||||
msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg
|
||||
liftClient SMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msg
|
||||
liftClient SMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msgFlags msg
|
||||
|
||||
agentNtfRegisterToken :: AgentMonad m => AgentClient -> NtfToken -> C.APublicVerifyKey -> C.PublicKeyX25519 -> m (NtfTokenId, C.PublicKeyX25519)
|
||||
agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey =
|
||||
@@ -520,6 +609,10 @@ agentNtfCheckToken :: AgentMonad m => AgentClient -> NtfTokenId -> NtfToken -> m
|
||||
agentNtfCheckToken c tknId NtfToken {ntfServer, ntfPrivKey} =
|
||||
withLogClient c ntfServer tknId "TCHK" $ \ntf -> ntfCheckToken ntf ntfPrivKey tknId
|
||||
|
||||
agentNtfReplaceToken :: AgentMonad m => AgentClient -> NtfTokenId -> NtfToken -> DeviceToken -> m ()
|
||||
agentNtfReplaceToken c tknId NtfToken {ntfServer, ntfPrivKey} token =
|
||||
withLogClient c ntfServer tknId "TRPL" $ \ntf -> ntfReplaceToken ntf ntfPrivKey tknId token
|
||||
|
||||
agentNtfDeleteToken :: AgentMonad m => AgentClient -> NtfTokenId -> NtfToken -> m ()
|
||||
agentNtfDeleteToken c tknId NtfToken {ntfServer, ntfPrivKey} =
|
||||
withLogClient c ntfServer tknId "TDEL" $ \ntf -> ntfDeleteToken ntf ntfPrivKey tknId
|
||||
@@ -528,6 +621,18 @@ agentNtfEnableCron :: AgentMonad m => AgentClient -> NtfTokenId -> NtfToken -> W
|
||||
agentNtfEnableCron c tknId NtfToken {ntfServer, ntfPrivKey} interval =
|
||||
withLogClient c ntfServer tknId "TCRN" $ \ntf -> ntfEnableCron ntf ntfPrivKey tknId interval
|
||||
|
||||
agentNtfCreateSubscription :: AgentMonad m => AgentClient -> NtfTokenId -> NtfToken -> SMPQueueNtf -> NtfPrivateSignKey -> m NtfSubscriptionId
|
||||
agentNtfCreateSubscription c tknId NtfToken {ntfServer, ntfPrivKey} smpQueue nKey =
|
||||
withLogClient c ntfServer tknId "SNEW" $ \ntf -> ntfCreateSubscription ntf ntfPrivKey (NewNtfSub tknId smpQueue nKey)
|
||||
|
||||
agentNtfCheckSubscription :: AgentMonad m => AgentClient -> NtfSubscriptionId -> NtfToken -> m NtfSubStatus
|
||||
agentNtfCheckSubscription c subId NtfToken {ntfServer, ntfPrivKey} =
|
||||
withLogClient c ntfServer subId "SCHK" $ \ntf -> ntfCheckSubscription ntf ntfPrivKey subId
|
||||
|
||||
agentNtfDeleteSubscription :: AgentMonad m => AgentClient -> NtfSubscriptionId -> NtfToken -> m ()
|
||||
agentNtfDeleteSubscription c subId NtfToken {ntfServer, ntfPrivKey} =
|
||||
withLogClient c ntfServer subId "SDEL" $ \ntf -> ntfDeleteSubscription ntf ntfPrivKey subId
|
||||
|
||||
agentCbEncrypt :: AgentMonad m => SndQueue -> Maybe C.PublicKeyX25519 -> ByteString -> m ByteString
|
||||
agentCbEncrypt SndQueue {e2eDhSecret} e2ePubKey msg = do
|
||||
cmNonce <- liftIO C.randomCbNonce
|
||||
@@ -565,4 +670,82 @@ cryptoError = \case
|
||||
C.CryptoHeaderError _ -> AGENT A_ENCRYPTION
|
||||
C.AESDecryptError -> AGENT A_ENCRYPTION
|
||||
C.CBDecryptError -> AGENT A_ENCRYPTION
|
||||
C.CERatchetDuplicateMessage -> AGENT A_DUPLICATE
|
||||
e -> INTERNAL $ show e
|
||||
|
||||
endAgentOperation :: AgentClient -> AgentOperation -> STM ()
|
||||
endAgentOperation c op = endOperation c op $ case op of
|
||||
AORcvNetwork ->
|
||||
suspendOperation c AOMsgDelivery $
|
||||
suspendSendingAndDatabase c
|
||||
AOMsgDelivery ->
|
||||
suspendSendingAndDatabase c
|
||||
AOSndNetwork ->
|
||||
suspendOperation c AODatabase $
|
||||
notifySuspended c
|
||||
AODatabase ->
|
||||
notifySuspended c
|
||||
|
||||
suspendSendingAndDatabase :: AgentClient -> STM ()
|
||||
suspendSendingAndDatabase c =
|
||||
suspendOperation c AOSndNetwork $
|
||||
suspendOperation c AODatabase $
|
||||
notifySuspended c
|
||||
|
||||
suspendOperation :: AgentClient -> AgentOperation -> STM () -> STM ()
|
||||
suspendOperation c op endedAction = do
|
||||
n <- stateTVar (agentOpSel op c) $ \s -> (opsInProgress s, s {opSuspended = True})
|
||||
-- unsafeIOToSTM $ putStrLn $ "suspendOperation_ " <> show op <> " " <> show n
|
||||
when (n == 0) $ whenSuspending c endedAction
|
||||
|
||||
notifySuspended :: AgentClient -> STM ()
|
||||
notifySuspended c = do
|
||||
-- unsafeIOToSTM $ putStrLn "notifySuspended"
|
||||
writeTBQueue (subQ c) ("", "", SUSPENDED)
|
||||
writeTVar (agentState c) ASSuspended
|
||||
|
||||
endOperation :: AgentClient -> AgentOperation -> STM () -> STM ()
|
||||
endOperation c op endedAction = do
|
||||
(suspended, n) <- stateTVar (agentOpSel op c) $ \s ->
|
||||
let n = max 0 (opsInProgress s - 1)
|
||||
in ((opSuspended s, n), s {opsInProgress = n})
|
||||
-- unsafeIOToSTM $ putStrLn $ "endOperation: " <> show op <> " " <> show suspended <> " " <> show n
|
||||
when (suspended && n == 0) $ whenSuspending c endedAction
|
||||
|
||||
whenSuspending :: AgentClient -> STM () -> STM ()
|
||||
whenSuspending c = whenM ((== ASSuspending) <$> readTVar (agentState c))
|
||||
|
||||
beginAgentOperation :: AgentClient -> AgentOperation -> STM ()
|
||||
beginAgentOperation c op = do
|
||||
let opVar = agentOpSel op c
|
||||
s <- readTVar opVar
|
||||
-- unsafeIOToSTM $ putStrLn $ "beginOperation? " <> show op <> " " <> show (opsInProgress s)
|
||||
when (opSuspended s) retry
|
||||
-- unsafeIOToSTM $ putStrLn $ "beginOperation! " <> show op <> " " <> show (opsInProgress s + 1)
|
||||
writeTVar opVar $! s {opsInProgress = opsInProgress s + 1}
|
||||
|
||||
withStore' :: AgentMonad m => AgentClient -> (DB.Connection -> IO a) -> m a
|
||||
withStore' c action = withStore c $ fmap Right . action
|
||||
|
||||
withStore :: AgentMonad m => AgentClient -> (DB.Connection -> IO (Either StoreError a)) -> m a
|
||||
withStore c action = do
|
||||
st <- asks store
|
||||
atomically $ beginAgentOperation c AODatabase
|
||||
r <- liftIO $ withTransaction st action `E.catch` handleInternal
|
||||
atomically $ endAgentOperation c AODatabase
|
||||
liftEither $ first storeError r
|
||||
where
|
||||
handleInternal :: E.SomeException -> IO (Either StoreError a)
|
||||
handleInternal = pure . Left . SEInternal . bshow
|
||||
storeError :: StoreError -> AgentErrorType
|
||||
storeError = \case
|
||||
SEConnNotFound -> CONN NOT_FOUND
|
||||
SEConnDuplicate -> CONN DUPLICATE
|
||||
SEBadConnType CRcv -> CONN SIMPLEX
|
||||
SEBadConnType CSnd -> CONN SIMPLEX
|
||||
SEInvitationNotFound -> CMD PROHIBITED
|
||||
-- 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
|
||||
SEAgentError e -> e
|
||||
e -> INTERNAL $ show e
|
||||
|
||||
@@ -1,37 +1,55 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Env.SQLite
|
||||
( AgentConfig (..),
|
||||
( AgentMonad,
|
||||
AgentConfig (..),
|
||||
InitialAgentServers (..),
|
||||
defaultAgentConfig,
|
||||
defaultReconnectInterval,
|
||||
Env (..),
|
||||
newSMPAgentEnv,
|
||||
NtfSupervisor (..),
|
||||
NtfSupervisorCommand (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Time.Clock (NominalDiffTime, nominalDay)
|
||||
import Data.Word (Word16)
|
||||
import Network.Socket
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.Protocol (SMPServer)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
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.Notifications.Client (NtfServer)
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (TLS, Transport (..))
|
||||
import Simplex.Messaging.Version
|
||||
import System.Random (StdGen, newStdGen)
|
||||
import UnliftIO (Async)
|
||||
import UnliftIO.STM
|
||||
|
||||
-- | Agent monad with MonadReader Env and MonadError AgentErrorType
|
||||
type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m)
|
||||
|
||||
data InitialAgentServers = InitialAgentServers
|
||||
{ smp :: NonEmpty SMPServer,
|
||||
ntf :: [NtfServer]
|
||||
@@ -43,16 +61,21 @@ data AgentConfig = AgentConfig
|
||||
connIdBytes :: Int,
|
||||
tbqSize :: Natural,
|
||||
dbFile :: FilePath,
|
||||
dbPoolSize :: Int,
|
||||
yesToMigrations :: Bool,
|
||||
smpCfg :: ProtocolClientConfig,
|
||||
ntfCfg :: ProtocolClientConfig,
|
||||
reconnectInterval :: RetryInterval,
|
||||
helloTimeout :: NominalDiffTime,
|
||||
resubscriptionConcurrency :: Int,
|
||||
ntfCron :: Word16,
|
||||
ntfWorkerThrottle :: Int,
|
||||
ntfSubCheckInterval :: NominalDiffTime,
|
||||
ntfMaxMessages :: Int,
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath
|
||||
certificateFile :: FilePath,
|
||||
smpAgentVersion :: Version,
|
||||
smpAgentVRange :: VersionRange
|
||||
}
|
||||
|
||||
defaultReconnectInterval :: RetryInterval
|
||||
@@ -73,18 +96,23 @@ defaultAgentConfig =
|
||||
connIdBytes = 12,
|
||||
tbqSize = 64,
|
||||
dbFile = "smp-agent.db",
|
||||
dbPoolSize = 4,
|
||||
yesToMigrations = False,
|
||||
smpCfg = defaultClientConfig {defaultTransport = ("5223", transport @TLS)},
|
||||
ntfCfg = defaultClientConfig {defaultTransport = ("443", transport @TLS)},
|
||||
reconnectInterval = defaultReconnectInterval,
|
||||
helloTimeout = 2 * nominalDay,
|
||||
resubscriptionConcurrency = 16,
|
||||
ntfCron = 20, -- minutes
|
||||
ntfWorkerThrottle = 1000000, -- microseconds
|
||||
ntfSubCheckInterval = nominalDay,
|
||||
ntfMaxMessages = 4,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
-- ! we do not generate these
|
||||
caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
|
||||
privateKeyFile = "/etc/opt/simplex-agent/agent.key",
|
||||
certificateFile = "/etc/opt/simplex-agent/agent.crt"
|
||||
certificateFile = "/etc/opt/simplex-agent/agent.crt",
|
||||
smpAgentVersion = currentSMPAgentVersion,
|
||||
smpAgentVRange = supportedSMPAgentVRange
|
||||
}
|
||||
|
||||
data Env = Env
|
||||
@@ -92,13 +120,33 @@ data Env = Env
|
||||
store :: SQLiteStore,
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
clientCounter :: TVar Int,
|
||||
randomServer :: TVar StdGen
|
||||
randomServer :: TVar StdGen,
|
||||
ntfSupervisor :: NtfSupervisor
|
||||
}
|
||||
|
||||
newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env
|
||||
newSMPAgentEnv config@AgentConfig {dbFile, dbPoolSize, yesToMigrations} = do
|
||||
newSMPAgentEnv config@AgentConfig {dbFile, yesToMigrations} = do
|
||||
idsDrg <- newTVarIO =<< drgNew
|
||||
store <- liftIO $ createSQLiteStore dbFile dbPoolSize Migrations.app yesToMigrations
|
||||
store <- liftIO $ createSQLiteStore dbFile Migrations.app yesToMigrations
|
||||
clientCounter <- newTVarIO 0
|
||||
randomServer <- newTVarIO =<< liftIO newStdGen
|
||||
return Env {config, store, idsDrg, clientCounter, randomServer}
|
||||
ntfSupervisor <- atomically . newNtfSubSupervisor $ tbqSize config
|
||||
return Env {config, store, idsDrg, clientCounter, randomServer, ntfSupervisor}
|
||||
|
||||
data NtfSupervisor = NtfSupervisor
|
||||
{ ntfTkn :: TVar (Maybe NtfToken),
|
||||
ntfSubQ :: TBQueue (ConnId, NtfSupervisorCommand),
|
||||
ntfWorkers :: TMap NtfServer (TMVar (), Async ()),
|
||||
ntfSMPWorkers :: TMap SMPServer (TMVar (), Async ())
|
||||
}
|
||||
|
||||
data NtfSupervisorCommand = NSCCreate | NSCDelete | NSCSmpDelete | NSCNtfWorker NtfServer | NSCNtfSMPWorker SMPServer
|
||||
deriving (Show)
|
||||
|
||||
newNtfSubSupervisor :: Natural -> STM NtfSupervisor
|
||||
newNtfSubSupervisor qSize = do
|
||||
ntfTkn <- newTVar Nothing
|
||||
ntfSubQ <- newTBQueue qSize
|
||||
ntfWorkers <- TM.empty
|
||||
ntfSMPWorkers <- TM.empty
|
||||
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
( runNtfSupervisor,
|
||||
nsUpdateToken,
|
||||
nsRemoveNtfToken,
|
||||
sendNtfSubCommand,
|
||||
closeNtfSupervisor,
|
||||
getNtfServer,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.Async (Async, uninterruptibleCancel)
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Logger.Simple (logError, logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Control.Monad.Reader
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Fixed (Fixed (MkFixed), Pico)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Time (UTCTime, addUTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
|
||||
import Simplex.Messaging.Agent.Client
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..))
|
||||
import qualified Simplex.Messaging.Agent.Protocol as AP
|
||||
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
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (tshow, unlessM)
|
||||
import System.Random (randomR)
|
||||
import UnliftIO (async)
|
||||
import UnliftIO.Concurrent (forkIO, threadDelay)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
runNtfSupervisor :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
|
||||
runNtfSupervisor c = forever . handleError $ do
|
||||
ns <- asks ntfSupervisor
|
||||
cmd <- atomically . readTBQueue $ ntfSubQ ns
|
||||
runExceptT (processNtfSub c cmd) >>= \case
|
||||
Left e -> liftIO $ print e
|
||||
Right _ -> return ()
|
||||
where
|
||||
handleError = E.handle $ \(e :: E.SomeException) -> logError $ "runNtfSupervisor error " <> tshow e
|
||||
|
||||
processNtfSub :: forall m. AgentMonad m => AgentClient -> (ConnId, NtfSupervisorCommand) -> m ()
|
||||
processNtfSub c@AgentClient {subQ} (connId, cmd) = do
|
||||
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
|
||||
case cmd of
|
||||
NSCCreate -> do
|
||||
(a, RcvQueue {server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
|
||||
a <- liftIO $ getNtfSubscription db connId
|
||||
q <- ExceptT $ getRcvQueue db connId
|
||||
pure (a, q)
|
||||
logInfo $ "processNtfSub, NSCCreate - a = " <> tshow a
|
||||
case a of
|
||||
Nothing -> do
|
||||
withNtfServer c $ \ntfServer -> do
|
||||
case clientNtfCreds of
|
||||
Just ClientNtfCreds {notifierId} -> do
|
||||
let newSub = newNtfSubscription connId smpServer (Just notifierId) ntfServer NASKey
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' c $ \db -> createNtfSubscription db newSub (NtfSubNTFAction NSACreate) ts
|
||||
addNtfNTFWorker ntfServer
|
||||
Nothing -> do
|
||||
let newSub = newNtfSubscription connId smpServer Nothing ntfServer NASNew
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' c $ \db -> createNtfSubscription db newSub (NtfSubSMPAction NSASmpKey) ts
|
||||
addNtfSMPWorker smpServer
|
||||
(Just (sub@NtfSubscription {ntfSubStatus, ntfServer = subNtfServer}, action_)) -> do
|
||||
case action_ of
|
||||
-- action was set to NULL after worker internal error
|
||||
Nothing -> resetSubscription
|
||||
Just (action, _)
|
||||
-- subscription was marked for deletion / is being deleted
|
||||
| isDeleteNtfSubAction action -> do
|
||||
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
|
||||
then resetSubscription
|
||||
else withNtfServer c $ \ntfServer -> do
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' c $ \db ->
|
||||
supervisorUpdateNtfSubscription db sub {ntfServer} (NtfSubNTFAction NSACreate) ts
|
||||
addNtfNTFWorker ntfServer
|
||||
| otherwise -> case action of
|
||||
NtfSubNTFAction _ -> addNtfNTFWorker subNtfServer
|
||||
NtfSubSMPAction _ -> addNtfSMPWorker smpServer
|
||||
where
|
||||
resetSubscription :: m ()
|
||||
resetSubscription =
|
||||
withNtfServer c $ \ntfServer -> do
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' c $ \db ->
|
||||
supervisorUpdateNtfSubscription db sub {ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew} (NtfSubSMPAction NSASmpKey) ts
|
||||
addNtfSMPWorker smpServer
|
||||
NSCDelete -> do
|
||||
sub_ <- withStore' c $ \db -> do
|
||||
ts <- liftIO getCurrentTime
|
||||
supervisorUpdateNtfSubAction db connId (NtfSubNTFAction NSADelete) ts
|
||||
getNtfSubscription db connId
|
||||
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
|
||||
case sub_ of
|
||||
(Just (NtfSubscription {ntfServer}, _)) -> addNtfNTFWorker ntfServer
|
||||
_ -> pure () -- err "NSCDelete - no subscription"
|
||||
NSCSmpDelete -> do
|
||||
withStore' c (`getRcvQueue` connId) >>= \case
|
||||
Right rq@RcvQueue {server = smpServer} -> do
|
||||
logInfo $ "processNtfSub, NSCSmpDelete - rq = " <> tshow rq
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' c $ \db -> supervisorUpdateNtfSubAction db connId (NtfSubSMPAction NSASmpDelete) ts
|
||||
addNtfSMPWorker smpServer
|
||||
_ -> err "NSCSmpDelete - no rcv queue"
|
||||
NSCNtfWorker ntfServer ->
|
||||
addNtfNTFWorker ntfServer
|
||||
NSCNtfSMPWorker smpServer ->
|
||||
addNtfSMPWorker smpServer
|
||||
where
|
||||
addNtfNTFWorker = addWorker ntfWorkers runNtfWorker
|
||||
addNtfSMPWorker = addWorker ntfSMPWorkers runNtfSMPWorker
|
||||
addWorker ::
|
||||
(NtfSupervisor -> TMap ProtocolServer (TMVar (), Async ())) ->
|
||||
(AgentClient -> ProtocolServer -> TMVar () -> m ()) ->
|
||||
ProtocolServer ->
|
||||
m ()
|
||||
addWorker wsSel runWorker srv = do
|
||||
ws <- asks $ wsSel . ntfSupervisor
|
||||
atomically (TM.lookup srv ws) >>= \case
|
||||
Nothing -> do
|
||||
doWork <- newTMVarIO ()
|
||||
worker <- async $ runWorker c srv doWork `E.finally` atomically (TM.delete srv ws)
|
||||
atomically $ TM.insert srv (doWork, worker) ws
|
||||
Just (doWork, _) ->
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
err :: String -> m ()
|
||||
err internalErrStr = atomically $ writeTBQueue subQ ("", connId, AP.ERR $ AP.INTERNAL internalErrStr)
|
||||
|
||||
withNtfServer :: AgentMonad m => AgentClient -> (NtfServer -> m ()) -> m ()
|
||||
withNtfServer c action = getNtfServer c >>= mapM_ action
|
||||
|
||||
runNtfWorker :: forall m. AgentMonad m => AgentClient -> NtfServer -> TMVar () -> m ()
|
||||
runNtfWorker c srv doWork = forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
nextSub_ <- withStore' c (`getNextNtfSubNTFAction` srv)
|
||||
logInfo $ "runNtfWorker, nextSub_ " <> tshow nextSub_
|
||||
case nextSub_ of
|
||||
Nothing -> noWorkToDo
|
||||
Just a@(NtfSubscription {connId}, _, _) -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \loop ->
|
||||
processAction a
|
||||
`catchError` ( \e -> do
|
||||
logInfo $ "runNtfWorker, error " <> tshow e
|
||||
case e of
|
||||
BROKER NETWORK -> loop
|
||||
BROKER TIMEOUT -> loop
|
||||
_ -> ntfInternalError c connId (show e)
|
||||
)
|
||||
throttle <- asks $ ntfWorkerThrottle . config
|
||||
liftIO $ threadDelay throttle
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
processAction :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> m ()
|
||||
processAction (sub@NtfSubscription {connId, smpServer, ntfSubId}, action, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (rescheduleAction doWork ts actionTs) $
|
||||
case action of
|
||||
NSACreate ->
|
||||
getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
RcvQueue {clientNtfCreds} <- withStore c (`getRcvQueue` connId)
|
||||
case clientNtfCreds of
|
||||
Just ClientNtfCreds {ntfPrivateKey, notifierId} -> do
|
||||
nSubId <- agentNtfCreateSubscription c tknId tkn (SMPQueueNtf smpServer notifierId) ntfPrivateKey
|
||||
-- TODO smaller retry until Active, less frequently (daily?) once Active
|
||||
let actionTs' = addUTCTime 30 ts
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NtfSubNTFAction NSACheck) actionTs'
|
||||
_ -> ntfInternalError c connId "NSACreate - no notifier queue credentials"
|
||||
_ -> ntfInternalError c connId "NSACreate - no active token"
|
||||
NSACheck ->
|
||||
getNtfToken >>= \case
|
||||
Just tkn ->
|
||||
case ntfSubId of
|
||||
Just nSubId ->
|
||||
agentNtfCheckSubscription c nSubId tkn >>= \case
|
||||
NSAuth -> do
|
||||
getNtfServer c >>= \case
|
||||
Just ntfServer -> do
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NtfSubSMPAction NSASmpKey) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
_ -> ntfInternalError c connId "NSACheck - failed to reset subscription, notification server not configured"
|
||||
status -> updateSubNextCheck ts status
|
||||
Nothing -> ntfInternalError c connId "NSACheck - no subscription ID"
|
||||
_ -> ntfInternalError c connId "NSACheck - no active token"
|
||||
NSADelete -> case ntfSubId of
|
||||
Just nSubId ->
|
||||
(getNtfToken >>= \tkn -> forM_ tkn $ agentNtfDeleteSubscription c nSubId)
|
||||
`E.finally` carryOnWithDeletion
|
||||
Nothing -> carryOnWithDeletion
|
||||
where
|
||||
carryOnWithDeletion :: m ()
|
||||
carryOnWithDeletion = do
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubId = Nothing, ntfSubStatus = NASOff} (NtfSubSMPAction NSASmpDelete) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
where
|
||||
updateSubNextCheck ts toStatus = do
|
||||
checkInterval <- asks $ ntfSubCheckInterval . config
|
||||
let nextCheckTs = addUTCTime checkInterval ts
|
||||
updateSub (NASCreated toStatus) (NtfSubNTFAction NSACheck) nextCheckTs
|
||||
updateSub toStatus toAction actionTs' =
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
|
||||
|
||||
runNtfSMPWorker :: forall m. AgentMonad m => AgentClient -> SMPServer -> TMVar () -> m ()
|
||||
runNtfSMPWorker c srv doWork = forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
nextSub_ <- withStore' c (`getNextNtfSubSMPAction` srv)
|
||||
logInfo $ "runNtfSMPWorker, nextSub_ " <> tshow nextSub_
|
||||
case nextSub_ of
|
||||
Nothing -> noWorkToDo
|
||||
Just a@(NtfSubscription {connId}, _, _) -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \loop ->
|
||||
processAction a
|
||||
`catchError` ( \e -> do
|
||||
logInfo $ "runNtfSMPWorker, error " <> tshow e
|
||||
case e of
|
||||
BROKER NETWORK -> loop
|
||||
BROKER TIMEOUT -> loop
|
||||
_ -> ntfInternalError c connId (show e)
|
||||
)
|
||||
throttle <- asks $ ntfWorkerThrottle . config
|
||||
liftIO $ threadDelay throttle
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
processAction :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> m ()
|
||||
processAction (sub@NtfSubscription {connId, ntfServer}, smpAction, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (rescheduleAction doWork ts actionTs) $
|
||||
case smpAction of
|
||||
NSASmpKey ->
|
||||
getNtfToken >>= \case
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
rq <- withStore c (`getRcvQueue` connId)
|
||||
C.SignAlg a <- asks (cmdSignAlg . config)
|
||||
(ntfPublicKey, ntfPrivateKey) <- liftIO $ C.generateSignatureKeyPair a
|
||||
(rcvNtfPubDhKey, rcvNtfPrivDhKey) <- liftIO C.generateKeyPair'
|
||||
(notifierId, rcvNtfSrvPubDhKey) <- enableQueueNotifications c rq ntfPublicKey rcvNtfPubDhKey
|
||||
let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
|
||||
withStore' c $ \db -> do
|
||||
setRcvQueueNtfCreds db connId $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
|
||||
updateNtfSubscription db sub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NtfSubNTFAction NSACreate) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCNtfWorker ntfServer)
|
||||
_ -> ntfInternalError c connId "NSASmpKey - no active token"
|
||||
NSASmpDelete -> do
|
||||
rq_ <- withStore' c $ \db -> do
|
||||
setRcvQueueNtfCreds db connId Nothing
|
||||
getRcvQueue db connId
|
||||
forM_ rq_ $ \rq -> disableQueueNotifications c rq
|
||||
withStore' c $ \db -> deleteNtfSubscription db connId
|
||||
|
||||
rescheduleAction :: AgentMonad m => TMVar () -> UTCTime -> UTCTime -> m Bool
|
||||
rescheduleAction doWork ts actionTs
|
||||
| actionTs <= ts = pure False
|
||||
| otherwise = do
|
||||
void . atomically $ tryTakeTMVar doWork
|
||||
void . forkIO $ do
|
||||
threadDelay $ diffInMicros actionTs ts
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
pure True
|
||||
|
||||
fromPico :: Pico -> Integer
|
||||
fromPico (MkFixed i) = i
|
||||
|
||||
diffInMicros :: UTCTime -> UTCTime -> Int
|
||||
diffInMicros a b = (`div` 1000000) . fromInteger . fromPico . nominalDiffTimeToSeconds $ diffUTCTime a b
|
||||
|
||||
ntfInternalError :: AgentMonad m => AgentClient -> ConnId -> String -> m ()
|
||||
ntfInternalError c@AgentClient {subQ} connId internalErrStr = do
|
||||
withStore' c $ \db -> setNullNtfSubscriptionAction db connId
|
||||
atomically $ writeTBQueue subQ ("", connId, AP.ERR $ AP.INTERNAL internalErrStr)
|
||||
|
||||
getNtfToken :: AgentMonad m => m (Maybe NtfToken)
|
||||
getNtfToken = do
|
||||
tkn <- asks $ ntfTkn . ntfSupervisor
|
||||
readTVarIO tkn
|
||||
|
||||
nsUpdateToken :: NtfSupervisor -> NtfToken -> STM ()
|
||||
nsUpdateToken ns tkn = writeTVar (ntfTkn ns) $ Just tkn
|
||||
|
||||
nsRemoveNtfToken :: NtfSupervisor -> STM ()
|
||||
nsRemoveNtfToken ns = writeTVar (ntfTkn ns) Nothing
|
||||
|
||||
sendNtfSubCommand :: NtfSupervisor -> (ConnId, NtfSupervisorCommand) -> STM ()
|
||||
sendNtfSubCommand ns cmd =
|
||||
readTVar (ntfTkn ns)
|
||||
>>= mapM_
|
||||
( \NtfToken {ntfTknStatus, ntfMode} ->
|
||||
when (ntfTknStatus == NTActive && ntfMode == NMInstant) $
|
||||
writeTBQueue (ntfSubQ ns) cmd
|
||||
)
|
||||
|
||||
closeNtfSupervisor :: NtfSupervisor -> IO ()
|
||||
closeNtfSupervisor ns = do
|
||||
cancelNtfWorkers_ $ ntfWorkers ns
|
||||
cancelNtfWorkers_ $ ntfSMPWorkers ns
|
||||
|
||||
cancelNtfWorkers_ :: TMap ProtocolServer (TMVar (), Async ()) -> IO ()
|
||||
cancelNtfWorkers_ wsVar = do
|
||||
ws <- atomically $ stateTVar wsVar (,M.empty)
|
||||
forM_ ws $ uninterruptibleCancel . snd
|
||||
|
||||
getNtfServer :: AgentMonad m => AgentClient -> m (Maybe NtfServer)
|
||||
getNtfServer c = do
|
||||
ntfServers <- readTVarIO $ ntfServers c
|
||||
case ntfServers of
|
||||
[] -> pure Nothing
|
||||
[srv] -> pure $ Just srv
|
||||
servers -> do
|
||||
gen <- asks randomServer
|
||||
atomically . stateTVar gen $
|
||||
first (Just . (servers !!)) . randomR (0, length servers - 1)
|
||||
@@ -32,8 +32,8 @@
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md
|
||||
module Simplex.Messaging.Agent.Protocol
|
||||
( -- * Protocol parameters
|
||||
smpAgentVersion,
|
||||
smpAgentVRange,
|
||||
currentSMPAgentVersion,
|
||||
supportedSMPAgentVRange,
|
||||
e2eEncConnInfoLength,
|
||||
e2eEncUserMsgLength,
|
||||
|
||||
@@ -82,6 +82,8 @@ module Simplex.Messaging.Agent.Protocol
|
||||
QueueStatus (..),
|
||||
ACorrId,
|
||||
AgentMsgId,
|
||||
NotificationsMode (..),
|
||||
NotificationInfo (..),
|
||||
|
||||
-- * Encode/decode
|
||||
serializeCommand,
|
||||
@@ -110,7 +112,7 @@ 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.Composition ((.:))
|
||||
import Data.Composition ((.:), (.:.))
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
@@ -118,9 +120,12 @@ import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Data.Time.ISO8601
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable ()
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.ToField
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Simplex.Messaging.Agent.QueryString
|
||||
@@ -132,7 +137,9 @@ import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol
|
||||
( ErrorType,
|
||||
MsgBody,
|
||||
MsgFlags,
|
||||
MsgId,
|
||||
NMsgMeta,
|
||||
SMPServer,
|
||||
SndPublicVerifyKey,
|
||||
SrvLoc (..),
|
||||
@@ -146,11 +153,11 @@ import Test.QuickCheck (Arbitrary (..))
|
||||
import Text.Read
|
||||
import UnliftIO.Exception (Exception)
|
||||
|
||||
smpAgentVersion :: Version
|
||||
smpAgentVersion = 1
|
||||
currentSMPAgentVersion :: Version
|
||||
currentSMPAgentVersion = 2
|
||||
|
||||
smpAgentVRange :: VersionRange
|
||||
smpAgentVRange = mkVersionRange 1 smpAgentVersion
|
||||
supportedSMPAgentVRange :: VersionRange
|
||||
supportedSMPAgentVRange = mkVersionRange 1 currentSMPAgentVersion
|
||||
|
||||
-- it is shorter to allow all handshake headers,
|
||||
-- including E2E (double-ratchet) parameters and
|
||||
@@ -212,21 +219,50 @@ data ACommand (p :: AParty) where
|
||||
END :: ACommand Agent
|
||||
DOWN :: SMPServer -> [ConnId] -> ACommand Agent
|
||||
UP :: SMPServer -> [ConnId] -> ACommand Agent
|
||||
SEND :: MsgBody -> ACommand Client
|
||||
SEND :: MsgFlags -> MsgBody -> ACommand Client
|
||||
MID :: AgentMsgId -> ACommand Agent
|
||||
SENT :: AgentMsgId -> ACommand Agent
|
||||
MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent
|
||||
MSG :: MsgMeta -> MsgBody -> ACommand Agent
|
||||
MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent
|
||||
ACK :: AgentMsgId -> ACommand Client
|
||||
OFF :: ACommand Client
|
||||
DEL :: ACommand Client
|
||||
OK :: ACommand Agent
|
||||
ERR :: AgentErrorType -> ACommand Agent
|
||||
SUSPENDED :: ACommand Agent
|
||||
|
||||
deriving instance Eq (ACommand p)
|
||||
|
||||
deriving instance Show (ACommand p)
|
||||
|
||||
data NotificationsMode = NMPeriodic | NMInstant
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding NotificationsMode where
|
||||
strEncode = \case
|
||||
NMPeriodic -> "PERIODIC"
|
||||
NMInstant -> "INSTANT"
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"PERIODIC" -> pure NMPeriodic
|
||||
"INSTANT" -> pure NMInstant
|
||||
_ -> fail "bad NotificationsMode"
|
||||
|
||||
instance ToJSON NotificationsMode where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance ToField NotificationsMode where toField = toField . strEncode
|
||||
|
||||
instance FromField NotificationsMode where fromField = blobFieldDecoder $ parseAll strP
|
||||
|
||||
data NotificationInfo = NotificationInfo
|
||||
{ ntfConnId :: ConnId,
|
||||
ntfTs :: SystemTime,
|
||||
ntfMsgMeta :: Maybe NMsgMeta
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ConnectionMode = CMInvitation | CMContact
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -287,7 +323,9 @@ data SMPConfirmation = SMPConfirmation
|
||||
-- | 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
|
||||
connInfo :: ConnInfo
|
||||
connInfo :: ConnInfo,
|
||||
-- | optional reply queues included in confirmation (added in agent protocol v2)
|
||||
smpReplyQueues :: [SMPQueueInfo]
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@@ -333,31 +371,40 @@ instance Encoding AgentMsgEnvelope where
|
||||
|
||||
-- SMP agent message formats (after double ratchet decryption,
|
||||
-- or in case of AgentInvitation - in plain text body)
|
||||
data AgentMessage = AgentConnInfo ConnInfo | AgentMessage APrivHeader AMessage
|
||||
data AgentMessage
|
||||
= AgentConnInfo ConnInfo
|
||||
| -- AgentConnInfoReply is only used in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation.
|
||||
-- It makes REPLY message unnecessary.
|
||||
AgentConnInfoReply (L.NonEmpty SMPQueueInfo) ConnInfo
|
||||
| AgentMessage APrivHeader AMessage
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding AgentMessage where
|
||||
smpEncode = \case
|
||||
AgentConnInfo cInfo -> smpEncode ('I', Tail cInfo)
|
||||
AgentConnInfoReply smpQueues cInfo -> smpEncode ('D', smpQueues, Tail cInfo) -- 'D' stands for "duplex"
|
||||
AgentMessage hdr aMsg -> smpEncode ('M', hdr, aMsg)
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
'I' -> AgentConnInfo . unTail <$> smpP
|
||||
'D' -> AgentConnInfoReply <$> smpP <*> (unTail <$> smpP)
|
||||
'M' -> AgentMessage <$> smpP <*> smpP
|
||||
_ -> fail "bad AgentMessage"
|
||||
|
||||
data AgentMessageType = AM_CONN_INFO | AM_HELLO_ | AM_REPLY_ | AM_A_MSG_
|
||||
data AgentMessageType = AM_CONN_INFO | AM_CONN_INFO_REPLY | AM_HELLO_ | AM_REPLY_ | AM_A_MSG_
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Encoding AgentMessageType where
|
||||
smpEncode = \case
|
||||
AM_CONN_INFO -> "C"
|
||||
AM_CONN_INFO_REPLY -> "D"
|
||||
AM_HELLO_ -> "H"
|
||||
AM_REPLY_ -> "R"
|
||||
AM_A_MSG_ -> "M"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'C' -> pure AM_CONN_INFO
|
||||
'D' -> pure AM_CONN_INFO_REPLY
|
||||
'H' -> pure AM_HELLO_
|
||||
'R' -> pure AM_REPLY_
|
||||
'M' -> pure AM_A_MSG_
|
||||
@@ -366,8 +413,14 @@ instance Encoding AgentMessageType where
|
||||
agentMessageType :: AgentMessage -> AgentMessageType
|
||||
agentMessageType = \case
|
||||
AgentConnInfo _ -> AM_CONN_INFO
|
||||
AgentConnInfoReply {} -> AM_CONN_INFO_REPLY
|
||||
AgentMessage _ aMsg -> case aMsg of
|
||||
-- HELLO is used both in v1 and in v2, but differently.
|
||||
-- - in v1 (and, possibly, in v2 for simplex connections) can be sent multiple times,
|
||||
-- until the queue is secured - the OK response from the server instead of initial AUTH errors confirms it.
|
||||
-- - in v2 duplexHandshake it is sent only once, when it is known that the queue was secured.
|
||||
HELLO -> AM_HELLO_
|
||||
-- REPLY is only used in v1
|
||||
REPLY _ -> AM_REPLY_
|
||||
A_MSG _ -> AM_A_MSG_
|
||||
|
||||
@@ -765,6 +818,8 @@ data SMPAgentError
|
||||
A_VERSION
|
||||
| -- | cannot decrypt message
|
||||
A_ENCRYPTION
|
||||
| -- | duplicate message - this error is detected by ratchet decryption - this message will be ignored and not shown
|
||||
A_DUPLICATE
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON SMPAgentError where
|
||||
@@ -840,13 +895,13 @@ commandP =
|
||||
acptCmd = ACmd SClient .: ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString
|
||||
rjctCmd = ACmd SClient . RJCT <$> A.takeByteString
|
||||
infoCmd = ACmd SAgent . INFO <$> A.takeByteString
|
||||
downsResp = ACmd SAgent .: DOWN <$> strP <* A.space <*> connections
|
||||
upsResp = ACmd SAgent .: UP <$> strP <* A.space <*> connections
|
||||
sendCmd = ACmd SClient . SEND <$> A.takeByteString
|
||||
downsResp = ACmd SAgent .: DOWN <$> strP_ <*> connections
|
||||
upsResp = ACmd SAgent .: UP <$> strP_ <*> connections
|
||||
sendCmd = ACmd SClient .: SEND <$> smpP <* A.space <*> A.takeByteString
|
||||
msgIdResp = ACmd SAgent . MID <$> A.decimal
|
||||
sentResp = ACmd SAgent . SENT <$> A.decimal
|
||||
msgErrResp = ACmd SAgent .: MERR <$> A.decimal <* A.space <*> strP
|
||||
message = ACmd SAgent .: MSG <$> msgMetaP <* A.space <*> A.takeByteString
|
||||
message = ACmd SAgent .:. MSG <$> msgMetaP <* A.space <*> smpP <* A.space <*> A.takeByteString
|
||||
ackCmd = ACmd SClient . ACK <$> A.decimal
|
||||
connections = strP `A.sepBy'` (A.char ',')
|
||||
msgMetaP = do
|
||||
@@ -877,17 +932,18 @@ serializeCommand = \case
|
||||
END -> "END"
|
||||
DOWN srv conns -> B.unwords ["DOWN", strEncode srv, connections conns]
|
||||
UP srv conns -> B.unwords ["UP", strEncode srv, connections conns]
|
||||
SEND msgBody -> "SEND " <> serializeBinary msgBody
|
||||
SEND msgFlags msgBody -> "SEND " <> smpEncode msgFlags <> " " <> serializeBinary msgBody
|
||||
MID mId -> "MID " <> bshow mId
|
||||
SENT mId -> "SENT " <> bshow mId
|
||||
MERR mId e -> B.unwords ["MERR", bshow mId, strEncode e]
|
||||
MSG msgMeta msgBody -> B.unwords ["MSG", serializeMsgMeta msgMeta, serializeBinary msgBody]
|
||||
MSG msgMeta msgFlags msgBody -> B.unwords ["MSG", serializeMsgMeta msgMeta, smpEncode msgFlags, serializeBinary msgBody]
|
||||
ACK mId -> "ACK " <> bshow mId
|
||||
OFF -> "OFF"
|
||||
DEL -> "DEL"
|
||||
CON -> "CON"
|
||||
ERR e -> "ERR " <> strEncode e
|
||||
OK -> "OK"
|
||||
SUSPENDED -> "SUSPENDED"
|
||||
where
|
||||
showTs :: UTCTime -> ByteString
|
||||
showTs = B.pack . formatISO8601Millis
|
||||
@@ -953,8 +1009,8 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
|
||||
cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p))
|
||||
cmdWithMsgBody = \case
|
||||
SEND body -> SEND <$$> getBody body
|
||||
MSG msgMeta body -> MSG msgMeta <$$> getBody body
|
||||
SEND msgFlags body -> SEND msgFlags <$$> getBody body
|
||||
MSG msgMeta msgFlags body -> MSG msgMeta msgFlags <$$> getBody body
|
||||
JOIN qUri cInfo -> JOIN qUri <$$> getBody cInfo
|
||||
CONF confId cInfo -> CONF confId <$$> getBody cInfo
|
||||
LET confId cInfo -> LET confId <$$> getBody cInfo
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
|
||||
module Simplex.Messaging.Agent.Store where
|
||||
|
||||
import Control.Concurrent.STM (TVar)
|
||||
import Control.Exception (Exception)
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
@@ -19,71 +17,21 @@ import Data.Time (UTCTime)
|
||||
import Data.Type.Equality
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff, SkippedMsgKeys)
|
||||
import Simplex.Messaging.Notifications.Client
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfTknStatus, NtfTokenId)
|
||||
import Simplex.Messaging.Crypto.Ratchet (RatchetX448)
|
||||
import Simplex.Messaging.Protocol
|
||||
( MsgBody,
|
||||
MsgFlags,
|
||||
MsgId,
|
||||
NotifierId,
|
||||
NtfPrivateSignKey,
|
||||
NtfPublicVerifyKey,
|
||||
RcvDhSecret,
|
||||
RcvNtfDhSecret,
|
||||
RcvPrivateSignKey,
|
||||
SndPrivateSignKey,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
|
||||
-- * Store management
|
||||
|
||||
-- | Store class type. Defines store access methods for implementations.
|
||||
class Monad m => MonadAgentStore s m where
|
||||
-- Queue and Connection management
|
||||
createRcvConn :: s -> TVar ChaChaDRG -> ConnData -> RcvQueue -> SConnectionMode c -> m ConnId
|
||||
createSndConn :: s -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId
|
||||
getConn :: s -> ConnId -> m SomeConn
|
||||
getRcvConn :: s -> SMPServer -> SMP.RecipientId -> m SomeConn
|
||||
deleteConn :: s -> ConnId -> m ()
|
||||
upgradeRcvConnToDuplex :: s -> ConnId -> SndQueue -> m ()
|
||||
upgradeSndConnToDuplex :: s -> ConnId -> RcvQueue -> m ()
|
||||
setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m ()
|
||||
setRcvQueueConfirmedE2E :: s -> RcvQueue -> C.DhSecretX25519 -> m ()
|
||||
setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m ()
|
||||
|
||||
-- Confirmations
|
||||
createConfirmation :: s -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId
|
||||
acceptConfirmation :: s -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation
|
||||
getAcceptedConfirmation :: s -> ConnId -> m AcceptedConfirmation
|
||||
removeConfirmations :: s -> ConnId -> m ()
|
||||
|
||||
-- Invitations - sent via Contact connections
|
||||
createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId
|
||||
getInvitation :: s -> InvitationId -> m Invitation
|
||||
acceptInvitation :: s -> InvitationId -> ConnInfo -> m ()
|
||||
deleteInvitation :: s -> ConnId -> InvitationId -> m ()
|
||||
|
||||
-- Msg management
|
||||
updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
|
||||
createRcvMsg :: s -> ConnId -> RcvMsgData -> m ()
|
||||
updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
createSndMsg :: s -> ConnId -> SndMsgData -> m ()
|
||||
getPendingMsgData :: s -> ConnId -> InternalId -> m (Maybe RcvQueue, (AgentMessageType, MsgBody, InternalTs))
|
||||
getPendingMsgs :: s -> ConnId -> m [InternalId]
|
||||
checkRcvMsg :: s -> ConnId -> InternalId -> m ()
|
||||
deleteMsg :: s -> ConnId -> InternalId -> m ()
|
||||
|
||||
-- Double ratchet persistence
|
||||
createRatchetX3dhKeys :: s -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> m ()
|
||||
getRatchetX3dhKeys :: s -> ConnId -> m (C.PrivateKeyX448, C.PrivateKeyX448)
|
||||
createRatchet :: s -> ConnId -> RatchetX448 -> m ()
|
||||
getRatchet :: s -> ConnId -> m RatchetX448
|
||||
getSkippedMsgKeys :: s -> ConnId -> m SkippedMsgKeys
|
||||
updateRatchet :: s -> ConnId -> RatchetX448 -> SkippedMsgDiff -> m ()
|
||||
|
||||
-- Notification device token persistence
|
||||
createNtfToken :: s -> NtfToken -> m ()
|
||||
|
||||
getDeviceNtfToken :: s -> DeviceToken -> m (Maybe NtfToken, [NtfToken])
|
||||
updateNtfTokenRegistration :: s -> NtfToken -> NtfTokenId -> C.DhSecretX25519 -> m ()
|
||||
updateNtfToken :: s -> NtfToken -> NtfTknStatus -> Maybe NtfTknAction -> m ()
|
||||
removeNtfToken :: s -> NtfToken -> m ()
|
||||
import Simplex.Messaging.Version
|
||||
|
||||
-- * Queue types
|
||||
|
||||
@@ -103,7 +51,20 @@ data RcvQueue = RcvQueue
|
||||
-- | sender queue ID
|
||||
sndId :: Maybe SMP.SenderId,
|
||||
-- | queue status
|
||||
status :: QueueStatus
|
||||
status :: QueueStatus,
|
||||
-- | credentials used in context of notifications
|
||||
clientNtfCreds :: Maybe ClientNtfCreds
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ClientNtfCreds = ClientNtfCreds
|
||||
{ -- | key pair to be used by the notification server to sign transmissions
|
||||
ntfPublicKey :: NtfPublicVerifyKey,
|
||||
ntfPrivateKey :: NtfPrivateSignKey,
|
||||
-- | queue ID to be used by the notification server for NSUB command
|
||||
notifierId :: NotifierId,
|
||||
-- | shared DH secret used to encrypt/decrypt notification metadata (NMsgMeta) from server to recipient
|
||||
rcvNtfDhSecret :: RcvNtfDhSecret
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -183,7 +144,11 @@ instance Eq SomeConn where
|
||||
|
||||
deriving instance Show SomeConn
|
||||
|
||||
newtype ConnData = ConnData {connId :: ConnId}
|
||||
data ConnData = ConnData
|
||||
{ connId :: ConnId,
|
||||
connAgentVersion :: Version,
|
||||
duplexHandshake :: Maybe Bool -- added in agent protocol v2
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- * Confirmation types
|
||||
@@ -230,30 +195,42 @@ type PrevRcvMsgHash = MsgHash
|
||||
-- | Corresponds to `last_snd_msg_hash` in `connections` table
|
||||
type PrevSndMsgHash = MsgHash
|
||||
|
||||
-- * Message data containers - used on Msg creation to reduce number of parameters
|
||||
-- * Message data containers
|
||||
|
||||
data RcvMsgData = RcvMsgData
|
||||
{ msgMeta :: MsgMeta,
|
||||
msgType :: AgentMessageType,
|
||||
msgFlags :: MsgFlags,
|
||||
msgBody :: MsgBody,
|
||||
internalRcvId :: InternalRcvId,
|
||||
internalHash :: MsgHash,
|
||||
externalPrevSndHash :: MsgHash
|
||||
}
|
||||
|
||||
data RcvMsg = RcvMsg
|
||||
{ internalId :: InternalId,
|
||||
msgMeta :: MsgMeta,
|
||||
msgBody :: MsgBody,
|
||||
userAck :: Bool
|
||||
}
|
||||
|
||||
data SndMsgData = SndMsgData
|
||||
{ internalId :: InternalId,
|
||||
internalSndId :: InternalSndId,
|
||||
internalTs :: InternalTs,
|
||||
msgType :: AgentMessageType,
|
||||
msgFlags :: MsgFlags,
|
||||
msgBody :: MsgBody,
|
||||
internalHash :: MsgHash,
|
||||
prevMsgHash :: MsgHash
|
||||
}
|
||||
|
||||
data PendingMsg = PendingMsg
|
||||
{ connId :: ConnId,
|
||||
msgId :: InternalId
|
||||
data PendingMsgData = PendingMsgData
|
||||
{ msgId :: InternalId,
|
||||
msgType :: AgentMessageType,
|
||||
msgFlags :: MsgFlags,
|
||||
msgBody :: MsgBody,
|
||||
internalTs :: InternalTs
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@@ -321,4 +298,6 @@ data StoreError
|
||||
SEX3dhKeysNotFound
|
||||
| -- | Used in `getMsg` that is not implemented/used. TODO remove.
|
||||
SENotImplemented
|
||||
| -- | Used to wrap agent errors inside store operations to avoid race conditions
|
||||
SEAgentError AgentErrorType
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,8 @@ import qualified Database.SQLite3 as SQLite3
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
|
||||
data Migration = Migration {name :: String, up :: Text}
|
||||
deriving (Show)
|
||||
@@ -35,7 +37,9 @@ schemaMigrations :: [(String, Query)]
|
||||
schemaMigrations =
|
||||
[ ("20220101_initial", m20220101_initial),
|
||||
("20220301_snd_queue_keys", m20220301_snd_queue_keys),
|
||||
("20220322_notifications", m20220322_notifications)
|
||||
("20220322_notifications", m20220322_notifications),
|
||||
("20220607_v2", m20220608_v2),
|
||||
("m20220625_v2_ntf_mode", m20220625_v2_ntf_mode)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -18,7 +18,7 @@ CREATE TABLE ntf_servers (
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE ntf_tokens (
|
||||
provider TEXT NOT NULL, -- apn
|
||||
provider TEXT NOT NULL, -- apns
|
||||
device_token TEXT NOT NULL, -- ! this field is mislabeled and is actually saved as binary
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220404_ntf_subscriptions_draft where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220404_ntf_subscriptions_draft :: Query
|
||||
m20220404_ntf_subscriptions_draft =
|
||||
[sql|
|
||||
ALTER TABLE rcv_queues ADD COLUMN ntf_id BLOB;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN ntf_public_key BLOB;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN ntf_private_key BLOB;
|
||||
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues (host, port, ntf_id);
|
||||
|
||||
CREATE TABLE ntf_subscriptions (
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_sub_id BLOB NOT NULL,
|
||||
ntf_sub_status TEXT NOT NULL, -- new, created, active, pending, error_auth
|
||||
ntf_sub_action TEXT, -- if there is an action required on this subscription: create / check / token / delete
|
||||
ntf_sub_action_ts TEXT, -- the earliest time for the action, e.g. checks can be scheduled every X hours
|
||||
ntf_token TEXT NOT NULL, -- or BLOB?
|
||||
smp_host TEXT NOT NULL,
|
||||
smp_port TEXT NOT NULL,
|
||||
smp_ntf_id BLOB NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL, -- this is to check subscription status periodically to know when it was last checked
|
||||
PRIMARY KEY (ntf_host, ntf_port, ntf_sub_id),
|
||||
FOREIGN KEY (ntf_host, ntf_port) REFERENCES ntf_servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
FOREIGN KEY (smp_host, smp_port, smp_ntf_id) REFERENCES rcv_queues (host, port, ntf_id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
@@ -0,0 +1,50 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2 where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220608_v2 :: Query
|
||||
m20220608_v2 =
|
||||
[sql|
|
||||
ALTER TABLE messages ADD COLUMN msg_flags TEXT NULL;
|
||||
|
||||
ALTER TABLE conn_confirmations ADD COLUMN smp_reply_queues BLOB NULL;
|
||||
|
||||
ALTER TABLE connections ADD COLUMN duplex_handshake INTEGER NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE rcv_messages ADD COLUMN user_ack INTEGER NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN ntf_public_key BLOB;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN ntf_private_key BLOB;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN ntf_id BLOB;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN rcv_ntf_dh_secret BLOB;
|
||||
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues (host, port, ntf_id);
|
||||
|
||||
CREATE TABLE ntf_subscriptions (
|
||||
conn_id BLOB NOT NULL,
|
||||
smp_host TEXT NULL,
|
||||
smp_port TEXT NULL,
|
||||
smp_ntf_id BLOB,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_sub_id BLOB,
|
||||
ntf_sub_status TEXT NOT NULL, -- see NtfAgentSubStatus
|
||||
ntf_sub_action TEXT, -- if there is an action required on this subscription: NtfSubNTFAction
|
||||
ntf_sub_smp_action TEXT, -- action with SMP server: NtfSubSMPAction; only one of this and ntf_sub_action can (should) be not null in same record
|
||||
ntf_sub_action_ts TEXT, -- the earliest time for the action, e.g. checks can be scheduled every X hours
|
||||
updated_by_supervisor INTEGER NOT NULL DEFAULT 0, -- to be checked on updates by workers to not overwrite supervisor command (state still should be updated)
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (conn_id),
|
||||
FOREIGN KEY (smp_host, smp_port) REFERENCES servers (host, port)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
FOREIGN KEY (ntf_host, ntf_port) REFERENCES ntf_servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
@@ -0,0 +1,14 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220625_v2_ntf_mode :: Query
|
||||
m20220625_v2_ntf_mode =
|
||||
[sql|
|
||||
ALTER TABLE ntf_tokens ADD COLUMN ntf_mode TEXT NULL;
|
||||
|
||||
DELETE FROM ntf_tokens;
|
||||
|]
|
||||
@@ -19,6 +19,8 @@ CREATE TABLE connections(
|
||||
last_rcv_msg_hash BLOB NOT NULL DEFAULT x'',
|
||||
last_snd_msg_hash BLOB NOT NULL DEFAULT x'',
|
||||
smp_agent_version INTEGER NOT NULL DEFAULT 1
|
||||
,
|
||||
duplex_handshake INTEGER NULL DEFAULT 0
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE rcv_queues(
|
||||
host TEXT NOT NULL,
|
||||
@@ -34,6 +36,10 @@ CREATE TABLE rcv_queues(
|
||||
status TEXT NOT NULL,
|
||||
smp_server_version INTEGER NOT NULL DEFAULT 1,
|
||||
smp_client_version INTEGER,
|
||||
ntf_public_key BLOB,
|
||||
ntf_private_key BLOB,
|
||||
ntf_id BLOB,
|
||||
rcv_ntf_dh_secret BLOB,
|
||||
PRIMARY KEY(host, port, rcv_id),
|
||||
FOREIGN KEY(host, port) REFERENCES servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
@@ -64,6 +70,7 @@ CREATE TABLE messages(
|
||||
internal_snd_id INTEGER,
|
||||
msg_type BLOB NOT NULL, --(H)ELLO,(R)EPLY,(D)ELETE. Should SMP confirmation be saved too?
|
||||
msg_body BLOB NOT NULL DEFAULT x'',
|
||||
msg_flags TEXT NULL,
|
||||
PRIMARY KEY(conn_id, internal_id),
|
||||
FOREIGN KEY(conn_id, internal_rcv_id) REFERENCES rcv_messages
|
||||
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
@@ -80,6 +87,7 @@ CREATE TABLE rcv_messages(
|
||||
internal_hash BLOB NOT NULL,
|
||||
external_prev_snd_hash BLOB NOT NULL,
|
||||
integrity BLOB NOT NULL,
|
||||
user_ack INTEGER NULL DEFAULT 0,
|
||||
PRIMARY KEY(conn_id, internal_rcv_id),
|
||||
FOREIGN KEY(conn_id, internal_id) REFERENCES messages
|
||||
ON DELETE CASCADE
|
||||
@@ -104,6 +112,8 @@ CREATE TABLE conn_confirmations(
|
||||
accepted INTEGER NOT NULL,
|
||||
own_conn_info BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
,
|
||||
smp_reply_queues BLOB NULL
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE conn_invitations(
|
||||
invitation_id BLOB NOT NULL PRIMARY KEY,
|
||||
@@ -141,7 +151,7 @@ CREATE TABLE ntf_servers(
|
||||
PRIMARY KEY(ntf_host, ntf_port)
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE ntf_tokens(
|
||||
provider TEXT NOT NULL, -- apn
|
||||
provider TEXT NOT NULL, -- apns
|
||||
device_token TEXT NOT NULL, -- ! this field is mislabeled and is actually saved as binary
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
@@ -154,8 +164,31 @@ tkn_dh_secret BLOB, -- DH secret for e2e encryption of notifications
|
||||
tkn_status TEXT NOT NULL,
|
||||
tkn_action BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now')), -- this is to check token status periodically to know when it was last checked
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
ntf_mode TEXT NULL, -- this is to check token status periodically to know when it was last checked
|
||||
PRIMARY KEY(provider, device_token, ntf_host, ntf_port),
|
||||
FOREIGN KEY(ntf_host, ntf_port) REFERENCES ntf_servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id);
|
||||
CREATE TABLE ntf_subscriptions(
|
||||
conn_id BLOB NOT NULL,
|
||||
smp_host TEXT NULL,
|
||||
smp_port TEXT NULL,
|
||||
smp_ntf_id BLOB,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_sub_id BLOB,
|
||||
ntf_sub_status TEXT NOT NULL, -- see NtfAgentSubStatus
|
||||
ntf_sub_action TEXT, -- if there is an action required on this subscription: NtfSubNTFAction
|
||||
ntf_sub_smp_action TEXT, -- action with SMP server: NtfSubSMPAction; only one of this and ntf_sub_action can(should) be not null in same record
|
||||
ntf_sub_action_ts TEXT, -- the earliest time for the action, e.g. checks can be scheduled every X hours
|
||||
updated_by_supervisor INTEGER NOT NULL DEFAULT 0, -- to be checked on updates by workers to not overwrite supervisor command(state still should be updated)
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
PRIMARY KEY(conn_id),
|
||||
FOREIGN KEY(smp_host, smp_port) REFERENCES servers(host, port)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
FOREIGN KEY(ntf_host, ntf_port) REFERENCES ntf_servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|
||||
@@ -32,9 +32,11 @@ module Simplex.Messaging.Client
|
||||
-- * SMP protocol command functions
|
||||
createSMPQueue,
|
||||
subscribeSMPQueue,
|
||||
getSMPMessage,
|
||||
subscribeSMPQueueNotifications,
|
||||
secureSMPQueue,
|
||||
enableSMPQueueNotifications,
|
||||
disableSMPQueueNotifications,
|
||||
sendSMPMessage,
|
||||
ackSMPMessage,
|
||||
suspendSMPQueue,
|
||||
@@ -70,6 +72,7 @@ import Simplex.Messaging.Transport.Client (runTransportClient)
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (bshow, liftError, raceAny_)
|
||||
import Simplex.Messaging.Version
|
||||
import System.Timeout (timeout)
|
||||
|
||||
-- | 'SMPClient' is a handle used to send commands to a specific SMP server.
|
||||
@@ -79,6 +82,7 @@ data ProtocolClient msg = ProtocolClient
|
||||
{ action :: Async (),
|
||||
connected :: TVar Bool,
|
||||
sessionId :: SessionId,
|
||||
thVersion :: Version,
|
||||
protocolServer :: ProtocolServer,
|
||||
tcpTimeout :: Int,
|
||||
clientCorrId :: TVar Natural,
|
||||
@@ -104,7 +108,9 @@ data ProtocolClientConfig = ProtocolClientConfig
|
||||
-- | TCP keep-alive options, Nothing to skip enabling keep-alive
|
||||
tcpKeepAlive :: Maybe KeepAliveOpts,
|
||||
-- | period for SMP ping commands (microseconds)
|
||||
smpPing :: Int
|
||||
smpPing :: Int,
|
||||
-- | SMP client-server protocol version range
|
||||
smpServerVRange :: VersionRange
|
||||
}
|
||||
|
||||
-- | Default protocol client configuration.
|
||||
@@ -115,7 +121,8 @@ defaultClientConfig =
|
||||
defaultTransport = ("443", transport @TLS),
|
||||
tcpTimeout = 5_000_000,
|
||||
tcpKeepAlive = Just defaultKeepAliveOpts,
|
||||
smpPing = 600_000_000 -- 10min
|
||||
smpPing = 600_000_000, -- 10min
|
||||
smpServerVRange = supportedSMPServerVRange
|
||||
}
|
||||
|
||||
data Request msg = Request
|
||||
@@ -131,7 +138,7 @@ type Response msg = Either ProtocolClientError msg
|
||||
-- A single queue can be used for multiple 'SMPClient' instances,
|
||||
-- as 'SMPServerTransmission' includes server information.
|
||||
getProtocolClient :: forall msg. Protocol msg => ProtocolServer -> ProtocolClientConfig -> Maybe (TBQueue (ServerTransmission msg)) -> IO () -> IO (Either ProtocolClientError (ProtocolClient msg))
|
||||
getProtocolClient protocolServer cfg@ProtocolClientConfig {qSize, tcpTimeout, tcpKeepAlive, smpPing} msgQ disconnected =
|
||||
getProtocolClient protocolServer cfg@ProtocolClientConfig {qSize, tcpTimeout, tcpKeepAlive, smpPing, smpServerVRange} msgQ disconnected =
|
||||
(atomically mkProtocolClient >>= runClient useTransport)
|
||||
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
|
||||
where
|
||||
@@ -146,6 +153,7 @@ getProtocolClient protocolServer cfg@ProtocolClientConfig {qSize, tcpTimeout, tc
|
||||
ProtocolClient
|
||||
{ action = undefined,
|
||||
sessionId = undefined,
|
||||
thVersion = undefined,
|
||||
connected,
|
||||
protocolServer,
|
||||
tcpTimeout,
|
||||
@@ -165,7 +173,7 @@ getProtocolClient protocolServer cfg@ProtocolClientConfig {qSize, tcpTimeout, tc
|
||||
`finally` atomically (putTMVar thVar $ Left PCENetworkError)
|
||||
th_ <- tcpTimeout `timeout` atomically (takeTMVar thVar)
|
||||
pure $ case th_ of
|
||||
Just (Right THandle {sessionId}) -> Right c {action, sessionId}
|
||||
Just (Right THandle {sessionId, thVersion}) -> Right c {action, sessionId, thVersion}
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left PCENetworkError
|
||||
|
||||
@@ -177,13 +185,13 @@ getProtocolClient protocolServer cfg@ProtocolClientConfig {qSize, tcpTimeout, tc
|
||||
|
||||
client :: forall c. Transport c => TProxy c -> ProtocolClient msg -> TMVar (Either ProtocolClientError (THandle c)) -> c -> IO ()
|
||||
client _ c thVar h =
|
||||
runExceptT (protocolClientHandshake @msg h $ keyHash protocolServer) >>= \case
|
||||
runExceptT (protocolClientHandshake @msg h (keyHash protocolServer) smpServerVRange) >>= \case
|
||||
Left e -> atomically . putTMVar thVar . Left $ PCETransportError e
|
||||
Right th@THandle {sessionId} -> do
|
||||
Right th@THandle {sessionId, thVersion} -> do
|
||||
atomically $ do
|
||||
writeTVar (connected c) True
|
||||
putTMVar thVar $ Right th
|
||||
let c' = c {sessionId} :: ProtocolClient msg
|
||||
let c' = c {sessionId, thVersion} :: ProtocolClient msg
|
||||
-- TODO remove ping if 0 is passed (or Nothing?)
|
||||
raceAny_ [send c' th, process c', receive c' th, ping c']
|
||||
`finally` disconnected
|
||||
@@ -216,7 +224,7 @@ getProtocolClient protocolServer cfg@ProtocolClientConfig {qSize, tcpTimeout, tc
|
||||
Right r -> case protocolError r of
|
||||
Just e -> Left $ PCEProtocolError e
|
||||
_ -> Right r
|
||||
else Left PCEUnexpectedResponse
|
||||
else Left . PCEUnexpectedResponse $ bshow respOrErr
|
||||
where
|
||||
sendMsg :: QueueId -> Either ErrorType msg -> IO ()
|
||||
sendMsg qId = \case
|
||||
@@ -240,7 +248,7 @@ data ProtocolClientError
|
||||
-- e.g. server should respond `IDS` or `ERR` to `NEW` command,
|
||||
-- other responses would result in this error.
|
||||
-- Forwarded to the agent client as `ERR BROKER UNEXPECTED`.
|
||||
PCEUnexpectedResponse
|
||||
PCEUnexpectedResponse ByteString
|
||||
| -- | Used for TCP connection and command response timeouts.
|
||||
-- Forwarded to the agent client as `ERR BROKER TIMEOUT`.
|
||||
PCEResponseTimeout
|
||||
@@ -268,7 +276,7 @@ createSMPQueue ::
|
||||
createSMPQueue c rpKey rKey dhKey =
|
||||
sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey) >>= \case
|
||||
IDS qik -> pure qik
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- | Subscribe to the SMP queue.
|
||||
--
|
||||
@@ -279,7 +287,19 @@ subscribeSMPQueue c@ProtocolClient {protocolServer, sessionId, msgQ} rpKey rId =
|
||||
OK -> return ()
|
||||
cmd@MSG {} ->
|
||||
lift . atomically $ mapM_ (`writeTBQueue` (protocolServer, sessionId, rId, cmd)) msgQ
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- | 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
|
||||
--
|
||||
-- https://github.covm/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#receive-a-message-from-the-queue
|
||||
getSMPMessage :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT ProtocolClientError IO (Maybe SMP.SMPMsgMeta)
|
||||
getSMPMessage c@ProtocolClient {protocolServer, sessionId, msgQ} rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId GET >>= \case
|
||||
OK -> pure Nothing
|
||||
cmd@(MSG msgId msgTs msgFlags _) -> do
|
||||
lift . atomically $ mapM_ (`writeTBQueue` (protocolServer, sessionId, rId, cmd)) msgQ
|
||||
pure $ Just SMP.SMPMsgMeta {msgId, msgTs, msgFlags}
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- | Subscribe to the SMP queue notifications.
|
||||
--
|
||||
@@ -296,31 +316,37 @@ secureSMPQueue c rpKey rId senderKey = okSMPCommand (KEY senderKey) c rpKey rId
|
||||
-- | Enable notifications for the queue for push notifications server.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#enable-notifications-command
|
||||
enableSMPQueueNotifications :: SMPClient -> RcvPrivateSignKey -> RecipientId -> NtfPublicVerifyKey -> ExceptT ProtocolClientError IO NotifierId
|
||||
enableSMPQueueNotifications c rpKey rId notifierKey =
|
||||
sendSMPCommand c (Just rpKey) rId (NKEY notifierKey) >>= \case
|
||||
NID nId -> pure nId
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
enableSMPQueueNotifications :: SMPClient -> RcvPrivateSignKey -> RecipientId -> NtfPublicVerifyKey -> RcvNtfPublicDhKey -> ExceptT ProtocolClientError IO (NotifierId, RcvNtfPublicDhKey)
|
||||
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
|
||||
|
||||
-- | Disable notifications for the queue for push notifications server.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#disable-notifications-command
|
||||
disableSMPQueueNotifications :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT ProtocolClientError IO ()
|
||||
disableSMPQueueNotifications = okSMPCommand NDEL
|
||||
|
||||
-- | Send SMP message.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#send-message
|
||||
sendSMPMessage :: SMPClient -> Maybe SndPrivateSignKey -> SenderId -> MsgBody -> ExceptT ProtocolClientError IO ()
|
||||
sendSMPMessage c spKey sId msg =
|
||||
sendSMPCommand c spKey sId (SEND msg) >>= \case
|
||||
sendSMPMessage :: SMPClient -> Maybe SndPrivateSignKey -> SenderId -> MsgFlags -> MsgBody -> ExceptT ProtocolClientError IO ()
|
||||
sendSMPMessage c spKey sId flags msg =
|
||||
sendSMPCommand c spKey sId (SEND flags msg) >>= \case
|
||||
OK -> pure ()
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- | Acknowledge message delivery (server deletes the message).
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
|
||||
ackSMPMessage :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT ProtocolClientError IO ()
|
||||
ackSMPMessage c@ProtocolClient {protocolServer, sessionId, msgQ} rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId ACK >>= \case
|
||||
ackSMPMessage :: SMPClient -> RcvPrivateSignKey -> QueueId -> MsgId -> ExceptT ProtocolClientError IO ()
|
||||
ackSMPMessage c@ProtocolClient {protocolServer, sessionId, msgQ} rpKey rId msgId =
|
||||
sendSMPCommand c (Just rpKey) rId (ACK msgId) >>= \case
|
||||
OK -> return ()
|
||||
cmd@MSG {} ->
|
||||
lift . atomically $ mapM_ (`writeTBQueue` (protocolServer, sessionId, rId, cmd)) msgQ
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- | Irreversibly suspend SMP queue.
|
||||
-- The existing messages from the queue will still be delivered.
|
||||
@@ -339,7 +365,7 @@ okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateSignKey -> Queue
|
||||
okSMPCommand cmd c pKey qId =
|
||||
sendSMPCommand c (Just pKey) qId cmd >>= \case
|
||||
OK -> return ()
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- | Send SMP command
|
||||
sendSMPCommand :: PartyI p => SMPClient -> Maybe C.APrivateSignKey -> QueueId -> Command p -> ExceptT ProtocolClientError IO BrokerMsg
|
||||
@@ -347,9 +373,9 @@ sendSMPCommand c pKey qId cmd = sendProtocolCommand c pKey qId (Cmd sParty cmd)
|
||||
|
||||
-- | Send Protocol command
|
||||
sendProtocolCommand :: forall msg. ProtocolEncoding (ProtocolCommand msg) => ProtocolClient msg -> Maybe C.APrivateSignKey -> QueueId -> ProtocolCommand msg -> ExceptT ProtocolClientError IO msg
|
||||
sendProtocolCommand ProtocolClient {sndQ, sentCommands, clientCorrId, sessionId, tcpTimeout} pKey qId cmd = do
|
||||
sendProtocolCommand ProtocolClient {sndQ, sentCommands, clientCorrId, sessionId, thVersion, tcpTimeout} pKey qId cmd = do
|
||||
corrId <- lift_ getNextCorrId
|
||||
t <- signTransmission $ encodeTransmission sessionId (corrId, qId, cmd)
|
||||
t <- signTransmission $ encodeTransmission thVersion sessionId (corrId, qId, cmd)
|
||||
ExceptT $ sendRecv corrId t
|
||||
where
|
||||
lift_ :: STM a -> ExceptT ProtocolClientError IO a
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Simplex.Messaging.Client.Agent where
|
||||
@@ -28,6 +29,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, ProtocolServer (..), QueueId, SMPServer)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (catchAll_, tryE, whenM, ($>>=))
|
||||
import System.Timeout (timeout)
|
||||
import UnliftIO (async, forConcurrently_)
|
||||
@@ -61,7 +63,7 @@ data SMPClientAgentConfig = SMPClientAgentConfig
|
||||
defaultSMPClientAgentConfig :: SMPClientAgentConfig
|
||||
defaultSMPClientAgentConfig =
|
||||
SMPClientAgentConfig
|
||||
{ smpCfg = defaultClientConfig,
|
||||
{ smpCfg = defaultClientConfig {defaultTransport = ("5223", transport @TLS)},
|
||||
reconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = second,
|
||||
|
||||
@@ -250,6 +250,12 @@ deriving instance Eq (PrivateKey a)
|
||||
|
||||
deriving instance Show (PrivateKey a)
|
||||
|
||||
instance StrEncoding (PrivateKey X25519) where
|
||||
strEncode = strEncode . encodePrivKey
|
||||
{-# INLINE strEncode #-}
|
||||
strDecode = decodePrivKey
|
||||
{-# INLINE strDecode #-}
|
||||
|
||||
data APrivateKey
|
||||
= forall a.
|
||||
AlgorithmI a =>
|
||||
@@ -296,6 +302,12 @@ instance Encoding APrivateSignKey where
|
||||
smpDecode = decodePrivKey
|
||||
{-# INLINE smpDecode #-}
|
||||
|
||||
instance StrEncoding APrivateSignKey where
|
||||
strEncode = strEncode . encodePrivKey
|
||||
{-# INLINE strEncode #-}
|
||||
strDecode = decodePrivKey
|
||||
{-# INLINE strDecode #-}
|
||||
|
||||
data APublicVerifyKey
|
||||
= forall a.
|
||||
(AlgorithmI a, SignatureAlgorithm a) =>
|
||||
@@ -678,7 +690,9 @@ data CryptoError
|
||||
CERatchetHeader
|
||||
| -- | too many skipped messages
|
||||
CERatchetTooManySkipped
|
||||
| -- | duplicate message number (or, possibly, skipped message that failed to decrypt?)
|
||||
| -- | earlier message number (or, possibly, skipped message that failed to decrypt?)
|
||||
CERatchetEarlierMessage
|
||||
| -- | duplicate message number
|
||||
CERatchetDuplicateMessage
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
@@ -869,7 +883,7 @@ cbDecrypt secret (CbNonce nonce) packet
|
||||
tag = Poly1305.auth rs c
|
||||
|
||||
newtype CbNonce = CbNonce {unCbNonce :: ByteString}
|
||||
deriving (Show)
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding CbNonce where
|
||||
strEncode (CbNonce s) = strEncode s
|
||||
|
||||
@@ -416,7 +416,8 @@ rcDecrypt rc@Ratchet {rcRcv, rcAD = Str rcAD} rcMKSkipped msg' = do
|
||||
skipMessageKeys :: Word32 -> Ratchet a -> Either CryptoError (Ratchet a, SkippedMsgKeys)
|
||||
skipMessageKeys _ r@Ratchet {rcRcv = Nothing} = Right (r, M.empty)
|
||||
skipMessageKeys untilN r@Ratchet {rcRcv = Just rr@RcvRatchet {rcCKr, rcHKr}, rcNr}
|
||||
| rcNr > untilN = Left CERatchetDuplicateMessage
|
||||
| rcNr > untilN + 1 = Left CERatchetEarlierMessage
|
||||
| rcNr == untilN + 1 = Left CERatchetDuplicateMessage
|
||||
| rcNr + maxSkip < untilN = Left CERatchetTooManySkipped
|
||||
| rcNr == untilN = Right (r, M.empty)
|
||||
| otherwise =
|
||||
|
||||
@@ -47,66 +47,100 @@ class Encoding a where
|
||||
|
||||
instance Encoding Char where
|
||||
smpEncode = B.singleton
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = A.anyChar
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding Bool where
|
||||
smpEncode = \case
|
||||
True -> "T"
|
||||
False -> "F"
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
'T' -> pure True
|
||||
'F' -> pure False
|
||||
_ -> fail "invalid Bool"
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding Word16 where
|
||||
smpEncode = encodeWord16
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = decodeWord16 <$> A.take 2
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding Word32 where
|
||||
smpEncode = encodeWord32
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = decodeWord32 <$> A.take 4
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding Int64 where
|
||||
smpEncode i = w32 (i `shiftR` 32) <> w32 i
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = do
|
||||
l <- w32P
|
||||
r <- w32P
|
||||
pure $ (l `shiftL` 32) .|. r
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
w32 :: Int64 -> ByteString
|
||||
w32 = smpEncode @Word32 . fromIntegral
|
||||
{-# INLINE w32 #-}
|
||||
|
||||
w32P :: Parser Int64
|
||||
w32P = fromIntegral <$> smpP @Word32
|
||||
{-# INLINE w32P #-}
|
||||
|
||||
-- ByteStrings are assumed no longer than 255 bytes
|
||||
instance Encoding ByteString where
|
||||
smpEncode s = B.cons (lenEncode $ B.length s) s
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = A.take =<< lenP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
lenEncode :: Int -> Char
|
||||
lenEncode = w2c . fromIntegral
|
||||
{-# INLINE lenEncode #-}
|
||||
|
||||
lenP :: Parser Int
|
||||
lenP = fromIntegral . c2w <$> A.anyChar
|
||||
{-# INLINE lenP #-}
|
||||
|
||||
instance Encoding a => Encoding (Maybe a) where
|
||||
smpEncode s = maybe "0" (("1" <>) . smpEncode) s
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
'0' -> pure Nothing
|
||||
'1' -> Just <$> smpP
|
||||
_ -> fail "invalid Maybe tag"
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
newtype Tail = Tail {unTail :: ByteString}
|
||||
|
||||
instance Encoding Tail where
|
||||
smpEncode = unTail
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = Tail <$> A.takeByteString
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
-- newtype for encoding/decoding ByteStrings over 255 bytes with 2-bytes length prefix
|
||||
newtype Large = Large {unLarge :: ByteString}
|
||||
|
||||
instance Encoding Large where
|
||||
smpEncode (Large s) = smpEncode @Word16 (fromIntegral $ B.length s) <> s
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = do
|
||||
len <- fromIntegral <$> smpP @Word16
|
||||
Large <$> A.take len
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding SystemTime where
|
||||
smpEncode = smpEncode . systemSeconds
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = MkSystemTime <$> smpP <*> pure 0
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
-- lists encode/parse as a sequence of items prefixed with list length (as 1 byte)
|
||||
smpEncodeList :: Encoding a => [a] -> ByteString
|
||||
@@ -117,7 +151,9 @@ smpListP = (`A.count` smpP) =<< lenP
|
||||
|
||||
instance Encoding String where
|
||||
smpEncode = smpEncode . B.pack
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = B.unpack <$> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding a => Encoding (L.NonEmpty a) where
|
||||
smpEncode = smpEncodeList . L.toList
|
||||
@@ -128,20 +164,36 @@ instance Encoding a => Encoding (L.NonEmpty a) where
|
||||
|
||||
instance (Encoding a, Encoding b) => Encoding (a, b) where
|
||||
smpEncode (a, b) = smpEncode a <> smpEncode b
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (,) <$> smpP <*> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance (Encoding a, Encoding b, Encoding c) => Encoding (a, b, c) where
|
||||
smpEncode (a, b, c) = smpEncode a <> smpEncode b <> smpEncode c
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (,,) <$> smpP <*> smpP <*> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance (Encoding a, Encoding b, Encoding c, Encoding d) => Encoding (a, b, c, d) where
|
||||
smpEncode (a, b, c, d) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (,,,) <$> smpP <*> smpP <*> smpP <*> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e) => Encoding (a, b, c, d, e) where
|
||||
smpEncode (a, b, c, d, e) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e, Encoding f) => Encoding (a, b, c, d, e, f) where
|
||||
smpEncode (a, b, c, d, e, f) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e <> smpEncode f
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (,,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e, Encoding f, Encoding g) => Encoding (a, b, c, d, e, f, g) where
|
||||
smpEncode (a, b, c, d, e, f, g) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e <> smpEncode f <> smpEncode g
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (,,,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
@@ -32,6 +32,7 @@ import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock.System (SystemTime (..))
|
||||
import Data.Word (Word16)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
|
||||
@@ -78,11 +79,27 @@ instance FromJSON Str where
|
||||
|
||||
instance StrEncoding a => StrEncoding (Maybe a) where
|
||||
strEncode = maybe "" strEncode
|
||||
{-# INLINE strEncode #-}
|
||||
strP = optional strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding Word16 where
|
||||
strEncode = B.pack . show
|
||||
{-# INLINE strEncode #-}
|
||||
strP = A.decimal
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding Char where
|
||||
strEncode = smpEncode
|
||||
{-# INLINE strEncode #-}
|
||||
strP = strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding Bool where
|
||||
strEncode = smpEncode
|
||||
{-# INLINE strEncode #-}
|
||||
strP = smpP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding Int64 where
|
||||
strEncode = B.pack . show
|
||||
@@ -109,19 +126,27 @@ listItem = parseAll strP <$?> A.takeTill (== ',')
|
||||
|
||||
instance (StrEncoding a, StrEncoding b) => StrEncoding (a, b) where
|
||||
strEncode (a, b) = B.unwords [strEncode a, strEncode b]
|
||||
{-# INLINE strEncode #-}
|
||||
strP = (,) <$> strP_ <*> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance (StrEncoding a, StrEncoding b, StrEncoding c) => StrEncoding (a, b, c) where
|
||||
strEncode (a, b, c) = B.unwords [strEncode a, strEncode b, strEncode c]
|
||||
{-# INLINE strEncode #-}
|
||||
strP = (,,) <$> strP_ <*> strP_ <*> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d) => StrEncoding (a, b, c, d) where
|
||||
strEncode (a, b, c, d) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d]
|
||||
{-# INLINE strEncode #-}
|
||||
strP = (,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d, StrEncoding e) => StrEncoding (a, b, c, d, e) where
|
||||
strEncode (a, b, c, d, e) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d, strEncode e]
|
||||
{-# INLINE strEncode #-}
|
||||
strP = (,,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP_ <*> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
strP_ :: StrEncoding a => Parser a
|
||||
strP_ = strP <* A.space
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Client where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Word (Word16)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder)
|
||||
import Simplex.Messaging.Protocol (ProtocolServer)
|
||||
|
||||
type NtfServer = ProtocolServer
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
|
||||
type NtfClient = ProtocolClient NtfResponse
|
||||
|
||||
ntfRegisterToken :: NtfClient -> C.APrivateSignKey -> NewNtfEntity 'Token -> ExceptT ProtocolClientError IO (NtfTokenId, C.PublicKeyX25519)
|
||||
ntfRegisterToken c pKey newTkn =
|
||||
sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case
|
||||
NRId tknId dhKey -> pure (tknId, dhKey)
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
NRTknId tknId dhKey -> pure (tknId, dhKey)
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
ntfVerifyToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> NtfRegCode -> ExceptT ProtocolClientError IO ()
|
||||
ntfVerifyToken c pKey tknId code = okNtfCommand (TVFY code) c pKey tknId
|
||||
@@ -37,7 +27,10 @@ ntfCheckToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> ExceptT Protoco
|
||||
ntfCheckToken c pKey tknId =
|
||||
sendNtfCommand c (Just pKey) tknId TCHK >>= \case
|
||||
NRTkn stat -> pure stat
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
ntfReplaceToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> DeviceToken -> ExceptT ProtocolClientError IO ()
|
||||
ntfReplaceToken c pKey tknId token = okNtfCommand (TRPL token) c pKey tknId
|
||||
|
||||
ntfDeleteToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> ExceptT ProtocolClientError IO ()
|
||||
ntfDeleteToken = okNtfCommand TDEL
|
||||
@@ -45,17 +38,17 @@ ntfDeleteToken = okNtfCommand TDEL
|
||||
ntfEnableCron :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> Word16 -> ExceptT ProtocolClientError IO ()
|
||||
ntfEnableCron c pKey tknId int = okNtfCommand (TCRN int) c pKey tknId
|
||||
|
||||
ntfCreateSubsciption :: NtfClient -> C.APrivateSignKey -> NewNtfEntity 'Subscription -> ExceptT ProtocolClientError IO (NtfSubscriptionId, C.PublicKeyX25519)
|
||||
ntfCreateSubsciption c pKey newSub =
|
||||
ntfCreateSubscription :: NtfClient -> C.APrivateSignKey -> NewNtfEntity 'Subscription -> ExceptT ProtocolClientError IO NtfSubscriptionId
|
||||
ntfCreateSubscription c pKey newSub =
|
||||
sendNtfCommand c (Just pKey) "" (SNEW newSub) >>= \case
|
||||
NRId subId dhKey -> pure (subId, dhKey)
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
NRSubId subId -> pure subId
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
ntfCheckSubscription :: NtfClient -> C.APrivateSignKey -> NtfSubscriptionId -> ExceptT ProtocolClientError IO NtfSubStatus
|
||||
ntfCheckSubscription c pKey subId =
|
||||
sendNtfCommand c (Just pKey) subId SCHK >>= \case
|
||||
NRSub stat -> pure stat
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
ntfDeleteSubscription :: NtfClient -> C.APrivateSignKey -> NtfSubscriptionId -> ExceptT ProtocolClientError IO ()
|
||||
ntfDeleteSubscription = okNtfCommand SDEL
|
||||
@@ -68,65 +61,4 @@ okNtfCommand :: NtfEntityI e => NtfCommand e -> NtfClient -> C.APrivateSignKey -
|
||||
okNtfCommand cmd c pKey entId =
|
||||
sendNtfCommand c (Just pKey) entId cmd >>= \case
|
||||
NROk -> return ()
|
||||
_ -> throwE PCEUnexpectedResponse
|
||||
|
||||
data NtfTknAction
|
||||
= NTARegister
|
||||
| NTAVerify NtfRegCode -- code to verify token
|
||||
| NTACheck
|
||||
| NTACron Word16
|
||||
| NTADelete
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding NtfTknAction where
|
||||
smpEncode = \case
|
||||
NTARegister -> "R"
|
||||
NTAVerify code -> smpEncode ('V', code)
|
||||
NTACheck -> "C"
|
||||
NTACron interval -> smpEncode ('I', interval)
|
||||
NTADelete -> "D"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'R' -> pure NTARegister
|
||||
'V' -> NTAVerify <$> smpP
|
||||
'C' -> pure NTACheck
|
||||
'I' -> NTACron <$> smpP
|
||||
'D' -> pure NTADelete
|
||||
_ -> fail "bad NtfTknAction"
|
||||
|
||||
instance FromField NtfTknAction where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
instance ToField NtfTknAction where toField = toField . smpEncode
|
||||
|
||||
data NtfToken = NtfToken
|
||||
{ deviceToken :: DeviceToken,
|
||||
ntfServer :: NtfServer,
|
||||
ntfTokenId :: Maybe NtfTokenId,
|
||||
-- | key used by the ntf server to verify transmissions
|
||||
ntfPubKey :: C.APublicVerifyKey,
|
||||
-- | key used by the ntf client to sign transmissions
|
||||
ntfPrivKey :: C.APrivateSignKey,
|
||||
-- | client's DH keys (to repeat registration if necessary)
|
||||
ntfDhKeys :: C.KeyPair 'C.X25519,
|
||||
-- | shared DH secret used to encrypt/decrypt notifications e2e
|
||||
ntfDhSecret :: Maybe C.DhSecretX25519,
|
||||
-- | token status
|
||||
ntfTknStatus :: NtfTknStatus,
|
||||
-- | pending token action and the earliest time
|
||||
ntfTknAction :: Maybe NtfTknAction
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newNtfToken :: DeviceToken -> NtfServer -> C.ASignatureKeyPair -> C.KeyPair 'C.X25519 -> NtfToken
|
||||
newNtfToken deviceToken ntfServer (ntfPubKey, ntfPrivKey) ntfDhKeys =
|
||||
NtfToken
|
||||
{ deviceToken,
|
||||
ntfServer,
|
||||
ntfTokenId = Nothing,
|
||||
ntfPubKey,
|
||||
ntfPrivKey,
|
||||
ntfDhKeys,
|
||||
ntfDhSecret = Nothing,
|
||||
ntfTknStatus = NTNew,
|
||||
ntfTknAction = Just NTARegister
|
||||
}
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Protocol where
|
||||
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
@@ -29,7 +29,7 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Transport (ntfClientHandshake)
|
||||
import Simplex.Messaging.Parsers (fromTextField_)
|
||||
import Simplex.Messaging.Protocol hiding (Command (..), CommandTag (..))
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
|
||||
data NtfEntity = Token | Subscription
|
||||
deriving (Show)
|
||||
@@ -55,6 +55,7 @@ data NtfCommandTag (e :: NtfEntity) where
|
||||
TNEW_ :: NtfCommandTag 'Token
|
||||
TVFY_ :: NtfCommandTag 'Token
|
||||
TCHK_ :: NtfCommandTag 'Token
|
||||
TRPL_ :: NtfCommandTag 'Token
|
||||
TDEL_ :: NtfCommandTag 'Token
|
||||
TCRN_ :: NtfCommandTag 'Token
|
||||
SNEW_ :: NtfCommandTag 'Subscription
|
||||
@@ -71,6 +72,7 @@ instance NtfEntityI e => Encoding (NtfCommandTag e) where
|
||||
TNEW_ -> "TNEW"
|
||||
TVFY_ -> "TVFY"
|
||||
TCHK_ -> "TCHK"
|
||||
TRPL_ -> "TRPL"
|
||||
TDEL_ -> "TDEL"
|
||||
TCRN_ -> "TCRN"
|
||||
SNEW_ -> "SNEW"
|
||||
@@ -88,6 +90,7 @@ instance ProtocolMsgTag NtfCmdTag where
|
||||
"TNEW" -> Just $ NCT SToken TNEW_
|
||||
"TVFY" -> Just $ NCT SToken TVFY_
|
||||
"TCHK" -> Just $ NCT SToken TCHK_
|
||||
"TRPL" -> Just $ NCT SToken TRPL_
|
||||
"TDEL" -> Just $ NCT SToken TDEL_
|
||||
"TCRN" -> Just $ NCT SToken TCRN_
|
||||
"SNEW" -> Just $ NCT SSubscription SNEW_
|
||||
@@ -120,16 +123,18 @@ instance ToJSON NtfRegCode where
|
||||
|
||||
data NewNtfEntity (e :: NtfEntity) where
|
||||
NewNtfTkn :: DeviceToken -> C.APublicVerifyKey -> C.PublicKeyX25519 -> NewNtfEntity 'Token
|
||||
NewNtfSub :: NtfTokenId -> SMPQueueNtf -> NewNtfEntity 'Subscription -- NtfTokenId -> C.APublicVerifyKey -> SMPQueueNtf
|
||||
NewNtfSub :: NtfTokenId -> SMPQueueNtf -> NtfPrivateSignKey -> NewNtfEntity 'Subscription
|
||||
|
||||
deriving instance Show (NewNtfEntity e)
|
||||
|
||||
data ANewNtfEntity = forall e. NtfEntityI e => ANE (SNtfEntity e) (NewNtfEntity e)
|
||||
|
||||
deriving instance Show ANewNtfEntity
|
||||
|
||||
instance NtfEntityI e => Encoding (NewNtfEntity e) where
|
||||
smpEncode = \case
|
||||
NewNtfTkn tkn verifyKey dhPubKey -> smpEncode ('T', tkn, verifyKey, dhPubKey)
|
||||
NewNtfSub tknId smpQueue -> smpEncode ('S', tknId, smpQueue)
|
||||
NewNtfSub tknId smpQueue notifierKey -> smpEncode ('S', tknId, smpQueue, notifierKey)
|
||||
smpP = (\(ANE _ c) -> checkEntity c) <$?> smpP
|
||||
|
||||
instance Encoding ANewNtfEntity where
|
||||
@@ -137,7 +142,7 @@ instance Encoding ANewNtfEntity where
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'T' -> ANE SToken <$> (NewNtfTkn <$> smpP <*> smpP <*> smpP)
|
||||
'S' -> ANE SSubscription <$> (NewNtfSub <$> smpP <*> smpP)
|
||||
'S' -> ANE SSubscription <$> (NewNtfSub <$> smpP <*> smpP <*> smpP)
|
||||
_ -> fail "bad ANewNtfEntity"
|
||||
|
||||
instance Protocol NtfResponse where
|
||||
@@ -155,13 +160,15 @@ data NtfCommand (e :: NtfEntity) where
|
||||
TVFY :: NtfRegCode -> NtfCommand 'Token
|
||||
-- | check token status
|
||||
TCHK :: NtfCommand 'Token
|
||||
-- | replace device token (while keeping all existing subscriptions)
|
||||
TRPL :: DeviceToken -> NtfCommand 'Token
|
||||
-- | delete token - all subscriptions will be removed and no more notifications will be sent
|
||||
TDEL :: NtfCommand 'Token
|
||||
-- | enable periodic background notification to fetch the new messages - interval is in minutes, minimum is 20, 0 to disable
|
||||
TCRN :: Word16 -> NtfCommand 'Token
|
||||
-- | create SMP subscription
|
||||
SNEW :: NewNtfEntity 'Subscription -> NtfCommand 'Subscription
|
||||
-- | check SMP subscription status (response is STAT)
|
||||
-- | check SMP subscription status (response is SUB)
|
||||
SCHK :: NtfCommand 'Subscription
|
||||
-- | delete SMP subscription
|
||||
SDEL :: NtfCommand 'Subscription
|
||||
@@ -176,10 +183,11 @@ deriving instance Show NtfCmd
|
||||
|
||||
instance NtfEntityI e => ProtocolEncoding (NtfCommand e) where
|
||||
type Tag (NtfCommand e) = NtfCommandTag e
|
||||
encodeProtocol = \case
|
||||
encodeProtocol _v = \case
|
||||
TNEW newTkn -> e (TNEW_, ' ', newTkn)
|
||||
TVFY code -> e (TVFY_, ' ', code)
|
||||
TCHK -> e TCHK_
|
||||
TRPL tkn -> e (TRPL_, ' ', tkn)
|
||||
TDEL -> e TDEL_
|
||||
TCRN int -> e (TCRN_, ' ', int)
|
||||
SNEW newSub -> e (SNEW_, ' ', newSub)
|
||||
@@ -190,7 +198,7 @@ instance NtfEntityI e => ProtocolEncoding (NtfCommand e) where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
|
||||
protocolP tag = (\(NtfCmd _ c) -> checkEntity c) <$?> protocolP (NCT (sNtfEntity @e) tag)
|
||||
protocolP _v tag = (\(NtfCmd _ c) -> checkEntity c) <$?> protocolP _v (NCT (sNtfEntity @e) tag)
|
||||
|
||||
checkCredentials (sig, _, entityId, _) cmd = case cmd of
|
||||
-- TNEW and SNEW must have signature but NOT token/subscription IDs
|
||||
@@ -211,14 +219,15 @@ instance NtfEntityI e => ProtocolEncoding (NtfCommand e) where
|
||||
|
||||
instance ProtocolEncoding NtfCmd where
|
||||
type Tag NtfCmd = NtfCmdTag
|
||||
encodeProtocol (NtfCmd _ c) = encodeProtocol c
|
||||
encodeProtocol _v (NtfCmd _ c) = encodeProtocol _v c
|
||||
|
||||
protocolP = \case
|
||||
protocolP _v = \case
|
||||
NCT SToken tag ->
|
||||
NtfCmd SToken <$> case tag of
|
||||
TNEW_ -> TNEW <$> _smpP
|
||||
TVFY_ -> TVFY <$> _smpP
|
||||
TCHK_ -> pure TCHK
|
||||
TRPL_ -> TRPL <$> _smpP
|
||||
TDEL_ -> pure TDEL
|
||||
TCRN_ -> TCRN <$> _smpP
|
||||
NCT SSubscription tag ->
|
||||
@@ -231,7 +240,8 @@ instance ProtocolEncoding NtfCmd where
|
||||
checkCredentials t (NtfCmd e c) = NtfCmd e <$> checkCredentials t c
|
||||
|
||||
data NtfResponseTag
|
||||
= NRId_
|
||||
= NRTknId_
|
||||
| NRSubId_
|
||||
| NROk_
|
||||
| NRErr_
|
||||
| NRTkn_
|
||||
@@ -241,7 +251,8 @@ data NtfResponseTag
|
||||
|
||||
instance Encoding NtfResponseTag where
|
||||
smpEncode = \case
|
||||
NRId_ -> "ID"
|
||||
NRTknId_ -> "IDTKN" -- it should be "TID", "SID"
|
||||
NRSubId_ -> "IDSUB"
|
||||
NROk_ -> "OK"
|
||||
NRErr_ -> "ERR"
|
||||
NRTkn_ -> "TKN"
|
||||
@@ -251,7 +262,8 @@ instance Encoding NtfResponseTag where
|
||||
|
||||
instance ProtocolMsgTag NtfResponseTag where
|
||||
decodeTag = \case
|
||||
"ID" -> Just NRId_
|
||||
"IDTKN" -> Just NRTknId_
|
||||
"IDSUB" -> Just NRSubId_
|
||||
"OK" -> Just NROk_
|
||||
"ERR" -> Just NRErr_
|
||||
"TKN" -> Just NRTkn_
|
||||
@@ -260,7 +272,8 @@ instance ProtocolMsgTag NtfResponseTag where
|
||||
_ -> Nothing
|
||||
|
||||
data NtfResponse
|
||||
= NRId NtfEntityId C.PublicKeyX25519
|
||||
= NRTknId NtfEntityId C.PublicKeyX25519
|
||||
| NRSubId NtfEntityId
|
||||
| NROk
|
||||
| NRErr ErrorType
|
||||
| NRTkn NtfTknStatus
|
||||
@@ -270,8 +283,9 @@ data NtfResponse
|
||||
|
||||
instance ProtocolEncoding NtfResponse where
|
||||
type Tag NtfResponse = NtfResponseTag
|
||||
encodeProtocol = \case
|
||||
NRId entId dhKey -> e (NRId_, ' ', entId, dhKey)
|
||||
encodeProtocol _v = \case
|
||||
NRTknId entId dhKey -> e (NRTknId_, ' ', entId, dhKey)
|
||||
NRSubId entId -> e (NRSubId_, ' ', entId)
|
||||
NROk -> e NROk_
|
||||
NRErr err -> e (NRErr_, ' ', err)
|
||||
NRTkn stat -> e (NRTkn_, ' ', stat)
|
||||
@@ -281,8 +295,9 @@ instance ProtocolEncoding NtfResponse where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
|
||||
protocolP = \case
|
||||
NRId_ -> NRId <$> _smpP <*> smpP
|
||||
protocolP _v = \case
|
||||
NRTknId_ -> NRTknId <$> _smpP <*> smpP
|
||||
NRSubId_ -> NRSubId <$> _smpP
|
||||
NROk_ -> pure NROk
|
||||
NRErr_ -> NRErr <$> _smpP
|
||||
NRTkn_ -> NRTkn <$> _smpP
|
||||
@@ -290,8 +305,10 @@ instance ProtocolEncoding NtfResponse where
|
||||
NRPong_ -> pure NRPong
|
||||
|
||||
checkCredentials (_, _, entId, _) cmd = case cmd of
|
||||
-- ID response must not have queue ID
|
||||
NRId {} -> noEntity
|
||||
-- IDTKN response must not have queue ID
|
||||
NRTknId {} -> noEntity
|
||||
-- IDSUB response must not have queue ID
|
||||
NRSubId {} -> noEntity
|
||||
-- ERR response does not always have entity ID
|
||||
NRErr _ -> Right cmd
|
||||
-- PONG response must not have queue ID
|
||||
@@ -307,38 +324,50 @@ instance ProtocolEncoding NtfResponse where
|
||||
|
||||
data SMPQueueNtf = SMPQueueNtf
|
||||
{ smpServer :: ProtocolServer,
|
||||
notifierId :: NotifierId,
|
||||
notifierKey :: NtfPrivateSignKey
|
||||
notifierId :: NotifierId
|
||||
}
|
||||
deriving (Show)
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
instance Encoding SMPQueueNtf where
|
||||
smpEncode SMPQueueNtf {smpServer, notifierId, notifierKey} = smpEncode (smpServer, notifierId, notifierKey)
|
||||
smpEncode SMPQueueNtf {smpServer, notifierId} = smpEncode (smpServer, notifierId)
|
||||
smpP = do
|
||||
(smpServer, notifierId, notifierKey) <- smpP
|
||||
pure $ SMPQueueNtf smpServer notifierId notifierKey
|
||||
(smpServer, notifierId) <- smpP
|
||||
pure $ SMPQueueNtf {smpServer, notifierId}
|
||||
|
||||
data PushProvider = PPApns
|
||||
instance StrEncoding SMPQueueNtf where
|
||||
strEncode SMPQueueNtf {smpServer, notifierId} = strEncode smpServer <> "/" <> strEncode notifierId
|
||||
strP = SMPQueueNtf <$> strP <* A.char '/' <*> strP
|
||||
|
||||
data PushProvider = PPApnsDev | PPApnsProd | PPApnsTest
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
instance Encoding PushProvider where
|
||||
smpEncode = \case
|
||||
PPApns -> "A"
|
||||
PPApnsDev -> "AD"
|
||||
PPApnsProd -> "AP"
|
||||
PPApnsTest -> "AT"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'A' -> pure PPApns
|
||||
A.take 2 >>= \case
|
||||
"AD" -> pure PPApnsDev
|
||||
"AP" -> pure PPApnsProd
|
||||
"AT" -> pure PPApnsTest
|
||||
_ -> fail "bad PushProvider"
|
||||
|
||||
instance TextEncoding PushProvider where
|
||||
textEncode = \case
|
||||
PPApns -> "apple"
|
||||
textDecode = \case
|
||||
"apple" -> Just PPApns
|
||||
_ -> Nothing
|
||||
instance StrEncoding PushProvider where
|
||||
strEncode = \case
|
||||
PPApnsDev -> "apns_dev"
|
||||
PPApnsProd -> "apns_prod"
|
||||
PPApnsTest -> "apns_test"
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"apns_dev" -> pure PPApnsDev
|
||||
"apns_prod" -> pure PPApnsProd
|
||||
"apns_test" -> pure PPApnsTest
|
||||
_ -> fail "bad PushProvider"
|
||||
|
||||
instance FromField PushProvider where fromField = fromTextField_ textDecode
|
||||
instance FromField PushProvider where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
|
||||
|
||||
instance ToField PushProvider where toField = toField . textEncode
|
||||
instance ToField PushProvider where toField = toField . decodeLatin1 . strEncode
|
||||
|
||||
data DeviceToken = DeviceToken PushProvider ByteString
|
||||
deriving (Eq, Ord, Show)
|
||||
@@ -347,6 +376,18 @@ instance Encoding DeviceToken where
|
||||
smpEncode (DeviceToken p t) = smpEncode (p, t)
|
||||
smpP = DeviceToken <$> smpP <*> smpP
|
||||
|
||||
instance StrEncoding DeviceToken where
|
||||
strEncode (DeviceToken p t) = strEncode p <> " " <> t
|
||||
strP = DeviceToken <$> strP <* A.space <*> hexStringP
|
||||
where
|
||||
hexStringP =
|
||||
A.takeWhile (\c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) >>= \s ->
|
||||
if even (B.length s) then pure s else fail "odd number of hex characters"
|
||||
|
||||
instance ToJSON DeviceToken where
|
||||
toEncoding (DeviceToken pp t) = J.pairs $ "pushProvider" .= decodeLatin1 (strEncode pp) <> "token" .= decodeLatin1 t
|
||||
toJSON (DeviceToken pp t) = J.object ["pushProvider" .= decodeLatin1 (strEncode pp), "token" .= decodeLatin1 t]
|
||||
|
||||
type NtfEntityId = ByteString
|
||||
|
||||
type NtfSubscriptionId = NtfEntityId
|
||||
@@ -360,10 +401,14 @@ data NtfSubStatus
|
||||
NSPending
|
||||
| -- | connected and subscribed to SMP server
|
||||
NSActive
|
||||
| -- | NEND received (we currently do not support it)
|
||||
| -- | disconnected/unsubscribed from SMP server
|
||||
NSInactive
|
||||
| -- | END received
|
||||
NSEnd
|
||||
| -- | SMP AUTH error
|
||||
NSSMPAuth
|
||||
NSAuth
|
||||
| -- | SMP error other than AUTH
|
||||
NSErr ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Encoding NtfSubStatus where
|
||||
@@ -371,17 +416,25 @@ instance Encoding NtfSubStatus where
|
||||
NSNew -> "NEW"
|
||||
NSPending -> "PENDING" -- e.g. after SMP server disconnect/timeout while ntf server is retrying to connect
|
||||
NSActive -> "ACTIVE"
|
||||
NSInactive -> "INACTIVE"
|
||||
NSEnd -> "END"
|
||||
NSSMPAuth -> "SMP_AUTH"
|
||||
NSAuth -> "AUTH"
|
||||
NSErr err -> "ERR " <> err
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"NEW" -> pure NSNew
|
||||
"PENDING" -> pure NSPending
|
||||
"ACTIVE" -> pure NSActive
|
||||
"INACTIVE" -> pure NSInactive
|
||||
"END" -> pure NSEnd
|
||||
"SMP_AUTH" -> pure NSSMPAuth
|
||||
"AUTH" -> pure NSAuth
|
||||
"ERR" -> NSErr <$> (A.space *> A.takeByteString)
|
||||
_ -> fail "bad NtfSubStatus"
|
||||
|
||||
instance StrEncoding NtfSubStatus where
|
||||
strEncode = smpEncode
|
||||
strP = smpP
|
||||
|
||||
data NtfTknStatus
|
||||
= -- | Token created in DB
|
||||
NTNew
|
||||
@@ -415,6 +468,10 @@ instance Encoding NtfTknStatus where
|
||||
"EXPIRED" -> pure NTExpired
|
||||
_ -> fail "bad NtfTknStatus"
|
||||
|
||||
instance StrEncoding NtfTknStatus where
|
||||
strEncode = smpEncode
|
||||
strP = smpP
|
||||
|
||||
instance FromField NtfTknStatus where fromField = fromTextField_ $ either (const Nothing) Just . smpDecode . encodeUtf8
|
||||
|
||||
instance ToField NtfTknStatus where toField = toField . decodeLatin1 . smpEncode
|
||||
|
||||
@@ -10,31 +10,37 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Server where
|
||||
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (MonadRandom)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Functor (($>))
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock.System (getSystemTime)
|
||||
import Network.Socket (ServiceName)
|
||||
import Simplex.Messaging.Client (ProtocolClientError (..))
|
||||
import Simplex.Messaging.Client.Agent
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Env
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..), PushNotification (..), PushProviderError (..))
|
||||
import Simplex.Messaging.Notifications.Server.Store
|
||||
import Simplex.Messaging.Notifications.Server.StoreLog
|
||||
import Simplex.Messaging.Notifications.Transport
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), SignedTransmission, Transmission, encodeTransmission, tGet, tPut)
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), SMPServer, SignedTransmission, Transmission, encodeTransmission, tGet, tPut)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport (..), THandle (..), TProxy, Transport (..))
|
||||
import Simplex.Messaging.Transport.Server (runTransportServer)
|
||||
import Simplex.Messaging.Util
|
||||
import UnliftIO (async, uninterruptibleCancel)
|
||||
import UnliftIO.Concurrent (threadDelay)
|
||||
import System.Mem.Weak (deRefWeak)
|
||||
import UnliftIO (IOMode (..), async, uninterruptibleCancel)
|
||||
import UnliftIO.Concurrent (forkIO, killThread, mkWeakThreadId, threadDelay)
|
||||
import UnliftIO.Exception
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -50,7 +56,9 @@ ntfServer :: forall m. (MonadUnliftIO m, MonadReader NtfEnv m) => NtfServerConfi
|
||||
ntfServer NtfServerConfig {transports} started = do
|
||||
s <- asks subscriber
|
||||
ps <- asks pushServer
|
||||
raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports)
|
||||
subs <- readTVarIO =<< asks (subscriptions . store)
|
||||
void . forkIO $ resubscribe s subs
|
||||
raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports) `finally` stopServer
|
||||
where
|
||||
runServer :: (ServiceName, ATransport) -> m ()
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
@@ -60,35 +68,77 @@ ntfServer NtfServerConfig {transports} started = do
|
||||
runClient :: Transport c => TProxy c -> c -> m ()
|
||||
runClient _ h = do
|
||||
kh <- asks serverIdentity
|
||||
liftIO (runExceptT $ ntfServerHandshake h kh) >>= \case
|
||||
liftIO (runExceptT $ ntfServerHandshake h kh supportedNTFServerVRange) >>= \case
|
||||
Right th -> runNtfClientTransport th
|
||||
Left _ -> pure ()
|
||||
|
||||
ntfSubscriber :: forall m. MonadUnliftIO m => NtfSubscriber -> m ()
|
||||
ntfSubscriber NtfSubscriber {subQ, smpAgent = ca@SMPClientAgent {msgQ, agentQ}} = do
|
||||
stopServer :: m ()
|
||||
stopServer = do
|
||||
withNtfLog closeStoreLog
|
||||
asks (smpSubscribers . subscriber) >>= readTVarIO >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (liftIO . deRefWeak >=> mapM_ killThread))
|
||||
|
||||
resubscribe :: (MonadUnliftIO m, MonadReader NtfEnv m) => NtfSubscriber -> Map NtfSubscriptionId NtfSubData -> m ()
|
||||
resubscribe NtfSubscriber {newSubQ} subs = do
|
||||
d <- asks $ resubscribeDelay . config
|
||||
forM_ subs $ \sub -> do
|
||||
atomically $ writeTBQueue newSubQ $ NtfSub sub
|
||||
threadDelay d
|
||||
liftIO $ logInfo "SMP connections resubscribed"
|
||||
|
||||
ntfSubscriber :: forall m. (MonadUnliftIO m, MonadReader NtfEnv m) => NtfSubscriber -> m ()
|
||||
ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAgent {msgQ, agentQ}} = do
|
||||
raceAny_ [subscribe, receiveSMP, receiveAgent]
|
||||
where
|
||||
subscribe :: m ()
|
||||
subscribe = forever $ do
|
||||
atomically (readTBQueue subQ) >>= \case
|
||||
NtfSub NtfSubData {smpQueue} -> do
|
||||
let SMPQueueNtf {smpServer, notifierId, notifierKey} = smpQueue
|
||||
liftIO (runExceptT $ subscribeQueue ca smpServer ((SPNotifier, notifierId), notifierKey)) >>= \case
|
||||
Right _ -> pure () -- update subscription status
|
||||
Left _e -> pure ()
|
||||
subscribe =
|
||||
forever $
|
||||
atomically (readTBQueue newSubQ) >>= \case
|
||||
sub@(NtfSub NtfSubData {smpQueue = SMPQueueNtf {smpServer}}) -> do
|
||||
SMPSubscriber {newSubQ = subscriberSubQ} <- getSMPSubscriber smpServer
|
||||
atomically $ writeTQueue subscriberSubQ sub
|
||||
|
||||
getSMPSubscriber :: SMPServer -> m SMPSubscriber
|
||||
getSMPSubscriber smpServer =
|
||||
atomically (TM.lookup smpServer smpSubscribers) >>= maybe createSMPSubscriber pure
|
||||
where
|
||||
createSMPSubscriber = do
|
||||
sub@SMPSubscriber {subThreadId} <- atomically newSMPSubscriber
|
||||
atomically $ TM.insert smpServer sub smpSubscribers
|
||||
tId <- mkWeakThreadId =<< forkIO (runSMPSubscriber sub)
|
||||
atomically . writeTVar subThreadId $ Just tId
|
||||
pure sub
|
||||
|
||||
runSMPSubscriber :: SMPSubscriber -> m ()
|
||||
runSMPSubscriber SMPSubscriber {newSubQ = subscriberSubQ} =
|
||||
forever $
|
||||
atomically (peekTQueue subscriberSubQ)
|
||||
>>= \(NtfSub NtfSubData {smpQueue, notifierKey}) -> do
|
||||
updateSubStatus smpQueue NSPending
|
||||
let SMPQueueNtf {smpServer, notifierId} = smpQueue
|
||||
liftIO (runExceptT $ subscribeQueue ca smpServer ((SPNotifier, notifierId), notifierKey)) >>= \case
|
||||
Right _ -> do
|
||||
updateSubStatus smpQueue NSActive
|
||||
void . atomically $ readTQueue subscriberSubQ
|
||||
Left err -> do
|
||||
handleSubError smpQueue err
|
||||
case err of
|
||||
PCEResponseTimeout -> pure ()
|
||||
PCENetworkError -> pure ()
|
||||
_ -> void . atomically $ readTQueue subscriberSubQ
|
||||
|
||||
receiveSMP :: m ()
|
||||
receiveSMP = forever $ do
|
||||
(_srv, _sessId, _ntfId, msg) <- atomically $ readTBQueue msgQ
|
||||
(srv, _sessId, ntfId, msg) <- atomically $ readTBQueue msgQ
|
||||
let smpQueue = SMPQueueNtf srv ntfId
|
||||
case msg of
|
||||
SMP.NMSG -> do
|
||||
-- check when the last NMSG was received from this queue
|
||||
-- update timestamp
|
||||
-- check what was the last hidden notification was sent (and whether to this queue)
|
||||
-- decide whether it should be sent as hidden or visible
|
||||
-- construct and possibly encrypt notification
|
||||
-- send it
|
||||
pure ()
|
||||
SMP.NMSG nmsgNonce encNMsgMeta -> do
|
||||
ntfTs <- liftIO getSystemTime
|
||||
st <- asks store
|
||||
NtfPushServer {pushQ} <- asks pushServer
|
||||
atomically $
|
||||
findNtfSubscriptionToken st smpQueue
|
||||
>>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}))
|
||||
SMP.END -> updateSubStatus smpQueue NSEnd
|
||||
_ -> pure ()
|
||||
pure ()
|
||||
|
||||
@@ -96,37 +146,85 @@ ntfSubscriber NtfSubscriber {subQ, smpAgent = ca@SMPClientAgent {msgQ, agentQ}}
|
||||
forever $
|
||||
atomically (readTBQueue agentQ) >>= \case
|
||||
CAConnected _ -> pure ()
|
||||
CADisconnected _srv _subs -> do
|
||||
-- update subscription statuses
|
||||
pure ()
|
||||
CADisconnected srv subs ->
|
||||
forM_ subs $ \(_, ntfId) -> do
|
||||
let smpQueue = SMPQueueNtf srv ntfId
|
||||
updateSubStatus smpQueue NSInactive
|
||||
CAReconnected _ -> pure ()
|
||||
CAResubscribed _srv _sub -> do
|
||||
-- update subscription status
|
||||
pure ()
|
||||
CASubError _srv _sub _err -> do
|
||||
-- update subscription status
|
||||
pure ()
|
||||
CAResubscribed srv sub -> do
|
||||
let ntfId = snd sub
|
||||
smpQueue = SMPQueueNtf srv ntfId
|
||||
updateSubStatus smpQueue NSActive
|
||||
CASubError srv (_, ntfId) err ->
|
||||
handleSubError (SMPQueueNtf srv ntfId) err
|
||||
|
||||
ntfPush :: MonadUnliftIO m => NtfPushServer -> m ()
|
||||
ntfPush s@NtfPushServer {pushQ} = liftIO . forever . runExceptT $ do
|
||||
(tkn@NtfTknData {token = DeviceToken pp _, tknStatus}, ntf) <- atomically (readTBQueue pushQ)
|
||||
logDebug $ "sending push notification to " <> T.pack (show pp)
|
||||
handleSubError :: SMPQueueNtf -> ProtocolClientError -> m ()
|
||||
handleSubError smpQueue = \case
|
||||
PCEProtocolError AUTH -> updateSubStatus smpQueue NSAuth
|
||||
PCEProtocolError e -> updateErr "SMP error " e
|
||||
PCEIOError e -> updateErr "IOError " e
|
||||
PCEResponseError e -> updateErr "ResponseError " e
|
||||
PCEUnexpectedResponse r -> updateErr "UnexpectedResponse " r
|
||||
PCETransportError e -> updateErr "TransportError " e
|
||||
PCESignatureError e -> updateErr "SignatureError " e
|
||||
PCEResponseTimeout -> pure ()
|
||||
PCENetworkError -> pure ()
|
||||
where
|
||||
updateErr :: Show e => ByteString -> e -> m ()
|
||||
updateErr errType e = updateSubStatus smpQueue . NSErr $ errType <> bshow e
|
||||
|
||||
updateSubStatus smpQueue status = do
|
||||
st <- asks store
|
||||
atomically (findNtfSubscription st smpQueue)
|
||||
>>= mapM_
|
||||
( \NtfSubData {ntfSubId, subStatus} -> do
|
||||
atomically $ writeTVar subStatus status
|
||||
withNtfLog $ \sl -> logSubscriptionStatus sl ntfSubId status
|
||||
)
|
||||
|
||||
ntfPush :: forall m. (MonadUnliftIO m, MonadReader NtfEnv m) => NtfPushServer -> m ()
|
||||
ntfPush s@NtfPushServer {pushQ} = forever $ do
|
||||
(tkn@NtfTknData {ntfTknId, token = DeviceToken pp _, tknStatus}, ntf) <- atomically (readTBQueue pushQ)
|
||||
liftIO $ logDebug $ "sending push notification to " <> T.pack (show pp)
|
||||
status <- readTVarIO tknStatus
|
||||
case (status, ntf) of
|
||||
(_, PNVerification _) -> do
|
||||
-- TODO check token status
|
||||
deliverNotification pp tkn ntf
|
||||
atomically $ modifyTVar tknStatus $ \status' -> if status' == NTActive then NTActive else NTConfirmed
|
||||
deliverNotification pp tkn ntf >>= \case
|
||||
Right _ -> do
|
||||
status_ <- atomically $ stateTVar tknStatus $ \status' -> if status' == NTActive then (Nothing, NTActive) else (Just NTConfirmed, NTConfirmed)
|
||||
forM_ status_ $ \status' -> withNtfLog $ \sl -> logTokenStatus sl ntfTknId status'
|
||||
_ -> pure ()
|
||||
(NTActive, PNCheckMessages) -> do
|
||||
deliverNotification pp tkn ntf
|
||||
void $ deliverNotification pp tkn ntf
|
||||
(NTActive, PNMessage {}) -> do
|
||||
void $ deliverNotification pp tkn ntf
|
||||
_ -> do
|
||||
logError "bad notification token status"
|
||||
liftIO $ logError "bad notification token status"
|
||||
where
|
||||
deliverNotification :: PushProvider -> PushProviderClient
|
||||
deliverNotification pp tkn ntf = do
|
||||
deliverNotification :: PushProvider -> NtfTknData -> PushNotification -> m (Either PushProviderError ())
|
||||
deliverNotification pp tkn@NtfTknData {ntfTknId, tknStatus} ntf = do
|
||||
deliver <- liftIO $ getPushClient s pp
|
||||
-- TODO retry later based on the error
|
||||
deliver tkn ntf `catchError` \e -> logError (T.pack $ "Push provider error (" <> show pp <> "): " <> show e) >> throwError e
|
||||
liftIO (runExceptT $ deliver tkn ntf) >>= \case
|
||||
Right _ -> pure $ Right ()
|
||||
Left e -> case e of
|
||||
PPConnection _ -> retryDeliver
|
||||
PPRetryLater -> retryDeliver
|
||||
-- TODO alert
|
||||
PPCryptoError _ -> err e
|
||||
PPResponseError _ _ -> err e
|
||||
PPTokenInvalid -> updateTknStatus NTInvalid >> err e
|
||||
PPPermanentError -> err e
|
||||
where
|
||||
retryDeliver :: m (Either PushProviderError ())
|
||||
retryDeliver = do
|
||||
deliver <- liftIO $ newPushClient s pp
|
||||
liftIO (runExceptT $ deliver tkn ntf) >>= either err (pure . Right)
|
||||
updateTknStatus :: NtfTknStatus -> m ()
|
||||
updateTknStatus status = do
|
||||
atomically $ writeTVar tknStatus status
|
||||
withNtfLog $ \sl -> logTokenStatus sl ntfTknId status
|
||||
err e = logError (T.pack $ "Push provider error (" <> show pp <> "): " <> show e) $> Left e
|
||||
|
||||
runNtfClientTransport :: (Transport c, MonadUnliftIO m, MonadReader NtfEnv m) => THandle c -> m ()
|
||||
runNtfClientTransport th@THandle {sessionId} = do
|
||||
@@ -159,11 +257,14 @@ receive th NtfServerClient {rcvQ, sndQ, activeAt} = forever $ do
|
||||
write q t = atomically $ writeTBQueue q t
|
||||
|
||||
send :: (Transport c, MonadUnliftIO m) => THandle c -> NtfServerClient -> m ()
|
||||
send h NtfServerClient {sndQ, sessionId, activeAt} = forever $ do
|
||||
send h@THandle {thVersion = v} NtfServerClient {sndQ, sessionId, activeAt} = forever $ do
|
||||
t <- atomically $ readTBQueue sndQ
|
||||
void . liftIO $ tPut h (Nothing, encodeTransmission sessionId t)
|
||||
void . liftIO $ tPut h (Nothing, encodeTransmission v sessionId t)
|
||||
atomically . writeTVar activeAt =<< liftIO getSystemTime
|
||||
|
||||
-- instance Show a => Show (TVar a) where
|
||||
-- show x = unsafePerformIO $ show <$> readTVarIO x
|
||||
|
||||
data VerificationResult = VRVerified NtfRequest | VRFailed
|
||||
|
||||
verifyNtfTransmission ::
|
||||
@@ -183,17 +284,45 @@ verifyNtfTransmission (sig_, signed, (corrId, entId, _)) cmd = do
|
||||
else VRFailed
|
||||
NtfCmd SToken c -> do
|
||||
t_ <- atomically $ getNtfToken st entId
|
||||
pure $ case t_ of
|
||||
Just t@NtfTknData {tknVerifyKey}
|
||||
| verifyCmdSignature sig_ signed tknVerifyKey -> verifiedTknCmd t c
|
||||
| otherwise -> VRFailed
|
||||
_ -> maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
|
||||
_ -> pure VRFailed
|
||||
verifyToken t_ (`verifiedTknCmd` c)
|
||||
NtfCmd SSubscription c@(SNEW sub@(NewNtfSub tknId smpQueue _)) -> do
|
||||
s_ <- atomically $ findNtfSubscription st smpQueue
|
||||
case s_ of
|
||||
Nothing -> do
|
||||
-- TODO move active token check here to differentiate error
|
||||
t_ <- atomically $ getActiveNtfToken st tknId
|
||||
verifyToken' t_ $ VRVerified (NtfReqNew corrId (ANE SSubscription sub))
|
||||
Just s@NtfSubData {tokenId = subTknId} ->
|
||||
if subTknId == tknId
|
||||
then do
|
||||
-- TODO move active token check here to differentiate error
|
||||
t_ <- atomically $ getActiveNtfToken st subTknId
|
||||
verifyToken' t_ $ verifiedSubCmd s c
|
||||
else pure $ maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
|
||||
NtfCmd SSubscription c -> do
|
||||
s_ <- atomically $ getNtfSubscription st entId
|
||||
case s_ of
|
||||
Just s@NtfSubData {tokenId = subTknId} -> do
|
||||
-- TODO move active token check here to differentiate error
|
||||
t_ <- atomically $ getActiveNtfToken st subTknId
|
||||
verifyToken' t_ $ verifiedSubCmd s c
|
||||
_ -> pure $ maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
|
||||
where
|
||||
verifiedTknCmd t c = VRVerified (NtfReqCmd SToken (NtfTkn t) (corrId, entId, c))
|
||||
verifiedSubCmd s c = VRVerified (NtfReqCmd SSubscription (NtfSub s) (corrId, entId, c))
|
||||
verifyToken :: Maybe NtfTknData -> (NtfTknData -> VerificationResult) -> m VerificationResult
|
||||
verifyToken t_ positiveVerificationResult =
|
||||
pure $ case t_ of
|
||||
Just t@NtfTknData {tknVerifyKey} ->
|
||||
if verifyCmdSignature sig_ signed tknVerifyKey
|
||||
then positiveVerificationResult t
|
||||
else VRFailed
|
||||
_ -> maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
|
||||
verifyToken' :: Maybe NtfTknData -> VerificationResult -> m VerificationResult
|
||||
verifyToken' t_ = verifyToken t_ . const
|
||||
|
||||
client :: forall m. (MonadUnliftIO m, MonadReader NtfEnv m) => NtfServerClient -> NtfSubscriber -> NtfPushServer -> m ()
|
||||
client NtfServerClient {rcvQ, sndQ} NtfSubscriber {subQ = _} NtfPushServer {pushQ, intervalNotifiers} =
|
||||
client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPushServer {pushQ, intervalNotifiers} =
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= processCommand
|
||||
@@ -208,12 +337,12 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {subQ = _} NtfPushServer {push
|
||||
let dhSecret = C.dh' dhPubKey srvDhPrivKey
|
||||
tknId <- getId
|
||||
regCode <- getRegCode
|
||||
atomically $ do
|
||||
tkn <- mkNtfTknData tknId newTkn ks dhSecret regCode
|
||||
addNtfToken st tknId tkn
|
||||
writeTBQueue pushQ (tkn, PNVerification regCode)
|
||||
pure (corrId, "", NRId tknId srvDhPubKey)
|
||||
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey)}) (corrId, tknId, cmd) -> do
|
||||
tkn <- atomically $ mkNtfTknData tknId newTkn ks dhSecret regCode
|
||||
atomically $ addNtfToken st tknId tkn
|
||||
atomically $ writeTBQueue pushQ (tkn, PNVerification regCode)
|
||||
withNtfLog (`logCreateToken` tkn)
|
||||
pure (corrId, "", NRTknId tknId srvDhPubKey)
|
||||
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey), tknCronInterval}) (corrId, tknId, cmd) -> do
|
||||
status <- readTVarIO tknStatus
|
||||
(corrId,tknId,) <$> case cmd of
|
||||
TNEW (NewNtfTkn _ _ dhPubKey) -> do
|
||||
@@ -223,7 +352,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {subQ = _} NtfPushServer {push
|
||||
if tknDhSecret == dhSecret
|
||||
then do
|
||||
atomically $ writeTBQueue pushQ (tkn, PNVerification tknRegCode)
|
||||
pure $ NRId ntfTknId srvDhPubKey
|
||||
pure $ NRTknId ntfTknId srvDhPubKey
|
||||
else pure $ NRErr AUTH
|
||||
TVFY code -- this allows repeated verification for cases when client connection dropped before server response
|
||||
| (status == NTRegistered || status == NTConfirmed || status == NTActive) && tknRegCode == code -> do
|
||||
@@ -232,31 +361,52 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {subQ = _} NtfPushServer {push
|
||||
atomically $ writeTVar tknStatus NTActive
|
||||
tIds <- atomically $ removeInactiveTokenRegistrations st tkn
|
||||
forM_ tIds cancelInvervalNotifications
|
||||
withNtfLog $ \s -> logTokenStatus s tknId NTActive
|
||||
pure NROk
|
||||
| otherwise -> do
|
||||
logDebug "TVFY - incorrect code or token status"
|
||||
pure $ NRErr AUTH
|
||||
TCHK -> pure $ NRTkn status
|
||||
TCHK -> do
|
||||
logDebug "TCHK"
|
||||
pure $ NRTkn status
|
||||
TRPL token' -> do
|
||||
logDebug "TRPL - replace token"
|
||||
st <- asks store
|
||||
regCode <- getRegCode
|
||||
atomically $ do
|
||||
removeTokenRegistration st tkn
|
||||
writeTVar tknStatus NTRegistered
|
||||
addNtfToken st tknId tkn {token = token', tknRegCode = regCode}
|
||||
writeTBQueue pushQ (tkn, PNVerification regCode)
|
||||
withNtfLog $ \s -> logUpdateToken s tknId token' regCode
|
||||
pure NROk
|
||||
TDEL -> do
|
||||
logDebug "TDEL"
|
||||
st <- asks store
|
||||
atomically $ deleteNtfToken st tknId
|
||||
qs <- atomically $ deleteNtfToken st tknId
|
||||
forM_ qs $ \SMPQueueNtf {smpServer, notifierId} ->
|
||||
atomically $ removeSubscription ca smpServer (SPNotifier, notifierId)
|
||||
cancelInvervalNotifications tknId
|
||||
withNtfLog (`logDeleteToken` tknId)
|
||||
pure NROk
|
||||
TCRN 0 -> do
|
||||
logDebug "TCRN 0"
|
||||
atomically $ writeTVar tknCronInterval 0
|
||||
cancelInvervalNotifications tknId
|
||||
withNtfLog $ \s -> logTokenCron s tknId 0
|
||||
pure NROk
|
||||
TCRN int
|
||||
| int < 20 -> pure $ NRErr QUOTA
|
||||
| otherwise -> do
|
||||
logDebug "TCRN"
|
||||
atomically $ writeTVar tknCronInterval int
|
||||
atomically (TM.lookup tknId intervalNotifiers) >>= \case
|
||||
Nothing -> runIntervalNotifier int
|
||||
Just IntervalNotifier {interval, action} ->
|
||||
unless (interval == int) $ do
|
||||
uninterruptibleCancel action
|
||||
runIntervalNotifier int
|
||||
withNtfLog $ \s -> logTokenCron s tknId int
|
||||
pure NROk
|
||||
where
|
||||
runIntervalNotifier interval = do
|
||||
@@ -267,12 +417,37 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {subQ = _} NtfPushServer {push
|
||||
intervalNotifier delay = forever $ do
|
||||
threadDelay delay
|
||||
atomically $ writeTBQueue pushQ (tkn, PNCheckMessages)
|
||||
NtfReqNew corrId (ANE SSubscription _newSub) -> pure (corrId, "", NROk)
|
||||
NtfReqCmd SSubscription _sub (corrId, subId, cmd) ->
|
||||
NtfReqNew corrId (ANE SSubscription newSub) -> do
|
||||
logDebug "SNEW - new subscription"
|
||||
st <- asks store
|
||||
subId <- getId
|
||||
sub <- atomically $ mkNtfSubData subId newSub
|
||||
resp <-
|
||||
atomically (addNtfSubscription st subId sub) >>= \case
|
||||
Just _ -> atomically (writeTBQueue newSubQ $ NtfSub sub) $> NRSubId subId
|
||||
_ -> pure $ NRErr AUTH
|
||||
withNtfLog (`logCreateSubscription` sub)
|
||||
pure (corrId, "", resp)
|
||||
NtfReqCmd SSubscription (NtfSub NtfSubData {smpQueue = SMPQueueNtf {smpServer, notifierId}, notifierKey = registeredNKey, subStatus}) (corrId, subId, cmd) -> do
|
||||
status <- readTVarIO subStatus
|
||||
(corrId,subId,) <$> case cmd of
|
||||
SNEW _newSub -> pure NROk
|
||||
SCHK -> pure NROk
|
||||
SDEL -> pure NROk
|
||||
SNEW (NewNtfSub _ _ notifierKey) -> do
|
||||
logDebug "SNEW - existing subscription"
|
||||
-- TODO retry if subscription failed, if pending or AUTH do nothing
|
||||
pure $
|
||||
if notifierKey == registeredNKey
|
||||
then NRSubId subId
|
||||
else NRErr AUTH
|
||||
SCHK -> do
|
||||
logDebug "SCHK"
|
||||
pure $ NRSub status
|
||||
SDEL -> do
|
||||
logDebug "SDEL"
|
||||
st <- asks store
|
||||
atomically $ deleteNtfSubscription st subId
|
||||
atomically $ removeSubscription ca smpServer (SPNotifier, notifierId)
|
||||
withNtfLog (`logDeleteSubscription` subId)
|
||||
pure NROk
|
||||
PING -> pure NRPong
|
||||
getId :: m NtfEntityId
|
||||
getId = getRandomBytes =<< asks (subIdBytes . config)
|
||||
@@ -287,44 +462,5 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {subQ = _} NtfPushServer {push
|
||||
atomically (TM.lookupDelete tknId intervalNotifiers)
|
||||
>>= mapM_ (uninterruptibleCancel . action)
|
||||
|
||||
-- NReqCreate corrId tokenId smpQueue -> pure (corrId, "", NROk)
|
||||
-- do
|
||||
-- st <- asks store
|
||||
-- (pubDhKey, privDhKey) <- liftIO C.generateKeyPair'
|
||||
-- let dhSecret = C.dh' dhPubKey privDhKey
|
||||
-- sub <- atomically $ mkNtfSubsciption smpQueue token verifyKey dhSecret
|
||||
-- addSubRetry 3 st sub >>= \case
|
||||
-- Nothing -> pure (corrId, "", NRErr INTERNAL)
|
||||
-- Just sId -> do
|
||||
-- atomically $ writeTBQueue subQ sub
|
||||
-- pure (corrId, sId, NRSubId pubDhKey)
|
||||
-- where
|
||||
-- addSubRetry :: Int -> NtfSubscriptionsStore -> NtfSubsciption -> m (Maybe NtfSubsciptionId)
|
||||
-- addSubRetry 0 _ _ = pure Nothing
|
||||
-- addSubRetry n st sub = do
|
||||
-- sId <- getId
|
||||
-- -- create QueueRec record with these ids and keys
|
||||
-- atomically (addNtfSub st sId sub) >>= \case
|
||||
-- Nothing -> addSubRetry (n - 1) st sub
|
||||
-- _ -> pure $ Just sId
|
||||
-- getId :: m NtfSubsciptionId
|
||||
-- getId = do
|
||||
-- n <- asks $ subIdBytes . config
|
||||
-- gVar <- asks idsDrg
|
||||
-- atomically (randomBytes n gVar)
|
||||
-- NReqCommand sub@NtfSubsciption {tokenId, subStatus} (corrId, subId, cmd) ->
|
||||
-- (corrId,subId,) <$> case cmd of
|
||||
-- NCSubCreate tokenId smpQueue -> pure NROk
|
||||
-- do
|
||||
-- st <- asks store
|
||||
-- (pubDhKey, privDhKey) <- liftIO C.generateKeyPair'
|
||||
-- let dhSecret = C.dh' (dhPubKey newSub) privDhKey
|
||||
-- atomically (updateNtfSub st sub newSub dhSecret) >>= \case
|
||||
-- Nothing -> pure $ NRErr INTERNAL
|
||||
-- _ -> atomically $ do
|
||||
-- whenM ((== NSEnd) <$> readTVar status) $ writeTBQueue subQ sub
|
||||
-- pure $ NRSubId pubDhKey
|
||||
-- NCSubCheck -> NRStat <$> readTVarIO subStatus
|
||||
-- NCSubDelete -> do
|
||||
-- st <- asks store
|
||||
-- atomically (deleteNtfSub st subId) $> NROk
|
||||
withNtfLog :: (MonadUnliftIO m, MonadReader NtfEnv m) => (StoreLog 'WriteMode -> IO a) -> m ()
|
||||
withNtfLog action = liftIO . mapM_ action =<< asks storeLog
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Env where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Concurrent.Async (Async)
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -23,12 +23,15 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Notifications.Server.Store
|
||||
import Simplex.Messaging.Protocol (CorrId, Transmission)
|
||||
import Simplex.Messaging.Notifications.Server.StoreLog
|
||||
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)
|
||||
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
|
||||
data NtfServerConfig = NtfServerConfig
|
||||
@@ -41,6 +44,8 @@ data NtfServerConfig = NtfServerConfig
|
||||
smpAgentCfg :: SMPClientAgentConfig,
|
||||
apnsConfig :: APNSPushClientConfig,
|
||||
inactiveClientExpiration :: Maybe ExpirationConfig,
|
||||
storeLogFile :: Maybe FilePath,
|
||||
resubscribeDelay :: Int, -- microseconds
|
||||
-- CA certificate private key is not needed for initialization
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
@@ -59,6 +64,7 @@ data NtfEnv = NtfEnv
|
||||
subscriber :: NtfSubscriber,
|
||||
pushServer :: NtfPushServer,
|
||||
store :: NtfStore,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
serverIdentity :: C.KeyHash,
|
||||
tlsServerParams :: T.ServerParams,
|
||||
@@ -66,27 +72,39 @@ data NtfEnv = NtfEnv
|
||||
}
|
||||
|
||||
newNtfServerEnv :: (MonadUnliftIO m, MonadRandom m) => NtfServerConfig -> m NtfEnv
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, caCertificateFile, certificateFile, privateKeyFile} = do
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile} = do
|
||||
idsDrg <- newTVarIO =<< drgNew
|
||||
store <- atomically newNtfStore
|
||||
storeLog <- liftIO $ mapM (`readWriteNtfStore` store) storeLogFile
|
||||
subscriber <- atomically $ newNtfSubscriber subQSize smpAgentCfg
|
||||
pushServer <- atomically $ newNtfPushServer pushQSize apnsConfig
|
||||
-- TODO not creating APNS client on start to pass CI test, has to be replaced with mock APNS server
|
||||
void . liftIO $ newPushClient pushServer PPApns
|
||||
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
|
||||
pure NtfEnv {config, subscriber, pushServer, store, idsDrg, tlsServerParams, serverIdentity = C.KeyHash fp}
|
||||
pure NtfEnv {config, subscriber, pushServer, store, storeLog, idsDrg, tlsServerParams, serverIdentity = C.KeyHash fp}
|
||||
|
||||
data NtfSubscriber = NtfSubscriber
|
||||
{ subQ :: TBQueue (NtfEntityRec 'Subscription),
|
||||
{ smpSubscribers :: TMap SMPServer SMPSubscriber,
|
||||
newSubQ :: TBQueue (NtfEntityRec 'Subscription),
|
||||
smpAgent :: SMPClientAgent
|
||||
}
|
||||
|
||||
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> STM NtfSubscriber
|
||||
newNtfSubscriber qSize smpAgentCfg = do
|
||||
smpSubscribers <- TM.empty
|
||||
newSubQ <- newTBQueue qSize
|
||||
smpAgent <- newSMPClientAgent smpAgentCfg
|
||||
subQ <- newTBQueue qSize
|
||||
pure NtfSubscriber {smpAgent, subQ}
|
||||
pure NtfSubscriber {smpSubscribers, newSubQ, smpAgent}
|
||||
|
||||
data SMPSubscriber = SMPSubscriber
|
||||
{ newSubQ :: TQueue (NtfEntityRec 'Subscription),
|
||||
subThreadId :: TVar (Maybe (Weak ThreadId))
|
||||
}
|
||||
|
||||
newSMPSubscriber :: STM SMPSubscriber
|
||||
newSMPSubscriber = do
|
||||
newSubQ <- newTQueue
|
||||
subThreadId <- newTVar Nothing
|
||||
pure SMPSubscriber {newSubQ, subThreadId}
|
||||
|
||||
data NtfPushServer = NtfPushServer
|
||||
{ pushQ :: TBQueue (NtfTknData, PushNotification),
|
||||
@@ -109,11 +127,10 @@ newNtfPushServer qSize apnsConfig = do
|
||||
pure NtfPushServer {pushQ, pushClients, intervalNotifiers, apnsConfig}
|
||||
|
||||
newPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
|
||||
newPushClient NtfPushServer {apnsConfig, pushClients} = \case
|
||||
PPApns -> do
|
||||
c <- apnsPushProviderClient <$> createAPNSPushClient apnsConfig
|
||||
atomically $ TM.insert PPApns c pushClients
|
||||
pure c
|
||||
newPushClient NtfPushServer {apnsConfig, pushClients} pp = do
|
||||
c <- apnsPushProviderClient <$> createAPNSPushClient (apnsProviderHost pp) apnsConfig
|
||||
atomically $ TM.insert pp c pushClients
|
||||
pure c
|
||||
|
||||
getPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
|
||||
getPushClient s@NtfPushServer {pushClients} pp =
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Push.APNS where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Except
|
||||
import Crypto.Hash.Algorithms (SHA256 (..))
|
||||
@@ -46,7 +47,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Store (NtfTknData (..))
|
||||
import Simplex.Messaging.Protocol (NotifierId, SMPServer)
|
||||
import Simplex.Messaging.Protocol (EncNMsgMeta)
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
import System.Environment (getEnv)
|
||||
import UnliftIO.STM
|
||||
@@ -95,10 +96,24 @@ readECPrivateKey f = do
|
||||
|
||||
data PushNotification
|
||||
= PNVerification NtfRegCode
|
||||
| PNMessage SMPServer NotifierId
|
||||
| PNMessage PNMessageData
|
||||
| PNAlert Text
|
||||
| PNCheckMessages
|
||||
|
||||
data PNMessageData = PNMessageData
|
||||
{ smpQueue :: SMPQueueNtf,
|
||||
ntfTs :: SystemTime,
|
||||
nmsgNonce :: C.CbNonce,
|
||||
encNMsgMeta :: EncNMsgMeta
|
||||
}
|
||||
|
||||
instance StrEncoding PNMessageData where
|
||||
strEncode PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta} =
|
||||
strEncode (smpQueue, ntfTs, nmsgNonce, encNMsgMeta)
|
||||
strP = do
|
||||
(smpQueue, ntfTs, nmsgNonce, encNMsgMeta) <- strP
|
||||
pure PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}
|
||||
|
||||
data APNSNotification = APNSNotification {aps :: APNSNotificationBody, notificationData :: Maybe J.Value}
|
||||
deriving (Show, Generic)
|
||||
|
||||
@@ -107,9 +122,9 @@ instance ToJSON APNSNotification where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
data APNSNotificationBody
|
||||
= APNSAlert {alert :: APNSAlertBody, badge :: Maybe Int, sound :: Maybe Text, category :: Maybe Text}
|
||||
| APNSBackground {contentAvailable :: Int}
|
||||
= APNSBackground {contentAvailable :: Int}
|
||||
| APNSMutableContent {mutableContent :: Int, alert :: APNSAlertBody, category :: Maybe Text}
|
||||
| APNSAlert {alert :: APNSAlertBody, badge :: Maybe Int, sound :: Maybe Text, category :: Maybe Text}
|
||||
deriving (Show, Generic)
|
||||
|
||||
apnsJSONOptions :: J.Options
|
||||
@@ -176,23 +191,27 @@ data APNSPushClientConfig = APNSPushClientConfig
|
||||
paddedNtfLength :: Int,
|
||||
appName :: ByteString,
|
||||
appTeamId :: Text,
|
||||
apnsHost :: HostName,
|
||||
apnsPort :: ServiceName,
|
||||
http2cfg :: HTTP2ClientConfig
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
apnsProviderHost :: PushProvider -> HostName
|
||||
apnsProviderHost = \case
|
||||
PPApnsTest -> "localhost"
|
||||
PPApnsDev -> "api.sandbox.push.apple.com"
|
||||
PPApnsProd -> "api.push.apple.com"
|
||||
|
||||
defaultAPNSPushClientConfig :: APNSPushClientConfig
|
||||
defaultAPNSPushClientConfig =
|
||||
APNSPushClientConfig
|
||||
{ tokenTTL = 1200, -- 20 minutes
|
||||
{ tokenTTL = 1800, -- 30 minutes
|
||||
authKeyFileEnv = "APNS_KEY_FILE", -- the environment variables APNS_KEY_FILE and APNS_KEY_ID must be set, or the server would fail to start
|
||||
authKeyAlg = "ES256",
|
||||
authKeyIdEnv = "APNS_KEY_ID",
|
||||
paddedNtfLength = 256,
|
||||
paddedNtfLength = 512,
|
||||
appName = "chat.simplex.app",
|
||||
appTeamId = "5NN7GUYB6T",
|
||||
apnsHost = "api.sandbox.push.apple.com",
|
||||
apnsPort = "443",
|
||||
http2cfg = defaultHTTP2ClientConfig
|
||||
}
|
||||
@@ -203,19 +222,20 @@ data APNSPushClient = APNSPushClient
|
||||
jwtHeader :: JWTHeader,
|
||||
jwtToken :: TVar (JWTToken, SignedJWTToken),
|
||||
nonceDrg :: TVar ChaChaDRG,
|
||||
apnsHost :: HostName,
|
||||
apnsCfg :: APNSPushClientConfig
|
||||
}
|
||||
|
||||
createAPNSPushClient :: APNSPushClientConfig -> IO APNSPushClient
|
||||
createAPNSPushClient apnsCfg@APNSPushClientConfig {authKeyFileEnv, authKeyAlg, authKeyIdEnv, appTeamId} = do
|
||||
createAPNSPushClient :: HostName -> APNSPushClientConfig -> IO APNSPushClient
|
||||
createAPNSPushClient apnsHost apnsCfg@APNSPushClientConfig {authKeyFileEnv, authKeyAlg, authKeyIdEnv, appTeamId} = do
|
||||
https2Client <- newTVarIO Nothing
|
||||
void $ connectHTTPS2 apnsCfg https2Client
|
||||
void $ connectHTTPS2 apnsHost apnsCfg https2Client
|
||||
privateKey <- readECPrivateKey =<< getEnv authKeyFileEnv
|
||||
authKeyId <- T.pack <$> getEnv authKeyIdEnv
|
||||
let jwtHeader = JWTHeader {alg = authKeyAlg, kid = authKeyId}
|
||||
jwtToken <- newTVarIO =<< mkApnsJWTToken appTeamId jwtHeader privateKey
|
||||
nonceDrg <- drgNew >>= newTVarIO
|
||||
pure APNSPushClient {https2Client, privateKey, jwtHeader, jwtToken, nonceDrg, apnsCfg}
|
||||
pure APNSPushClient {https2Client, privateKey, jwtHeader, jwtToken, nonceDrg, apnsHost, apnsCfg}
|
||||
|
||||
getApnsJWTToken :: APNSPushClient -> IO SignedJWTToken
|
||||
getApnsJWTToken APNSPushClient {apnsCfg = APNSPushClientConfig {appTeamId, tokenTTL}, privateKey, jwtHeader, jwtToken} = do
|
||||
@@ -236,8 +256,8 @@ mkApnsJWTToken appTeamId jwtHeader privateKey = do
|
||||
signedJWT <- signedJWTToken privateKey jwt
|
||||
pure (jwt, signedJWT)
|
||||
|
||||
connectHTTPS2 :: APNSPushClientConfig -> TVar (Maybe HTTP2Client) -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
connectHTTPS2 APNSPushClientConfig {apnsHost, apnsPort, http2cfg} https2Client = do
|
||||
connectHTTPS2 :: HostName -> APNSPushClientConfig -> TVar (Maybe HTTP2Client) -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
connectHTTPS2 apnsHost APNSPushClientConfig {apnsPort, http2cfg} https2Client = do
|
||||
r <- getHTTP2Client apnsHost apnsPort http2cfg disconnected
|
||||
case r of
|
||||
Right client -> atomically . writeTVar https2Client $ Just client
|
||||
@@ -247,28 +267,31 @@ connectHTTPS2 APNSPushClientConfig {apnsHost, apnsPort, http2cfg} https2Client =
|
||||
disconnected = atomically $ writeTVar https2Client Nothing
|
||||
|
||||
getApnsHTTP2Client :: APNSPushClient -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getApnsHTTP2Client APNSPushClient {https2Client, apnsCfg} =
|
||||
readTVarIO https2Client >>= maybe (connectHTTPS2 apnsCfg https2Client) (pure . Right)
|
||||
getApnsHTTP2Client APNSPushClient {https2Client, apnsHost, apnsCfg} =
|
||||
readTVarIO https2Client >>= maybe (connectHTTPS2 apnsHost apnsCfg https2Client) (pure . Right)
|
||||
|
||||
disconnectApnsHTTP2Client :: APNSPushClient -> IO ()
|
||||
disconnectApnsHTTP2Client APNSPushClient {https2Client} =
|
||||
readTVarIO https2Client >>= mapM_ closeHTTP2Client >> atomically (writeTVar https2Client Nothing)
|
||||
|
||||
ntfCategoryCheckMessage :: Text
|
||||
ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
||||
|
||||
apnsNotification :: NtfTknData -> C.CbNonce -> Int -> PushNotification -> Either C.CryptoError APNSNotification
|
||||
apnsNotification NtfTknData {tknDhSecret} nonce paddedLen = \case
|
||||
PNVerification (NtfRegCode code) ->
|
||||
encrypt code $ \code' ->
|
||||
apn APNSBackground {contentAvailable = 1} . Just $ J.object ["verification" .= code', "nonce" .= nonce]
|
||||
PNMessage srv nId ->
|
||||
encrypt (strEncode srv <> "/" <> strEncode nId) $ \ntfQueue ->
|
||||
apn apnMutableContent . Just $ J.object ["checkMessage" .= ntfQueue, "nonce" .= nonce]
|
||||
apn APNSBackground {contentAvailable = 1} . Just $ J.object ["nonce" .= nonce, "verification" .= code']
|
||||
PNMessage pnMessageData ->
|
||||
encrypt (strEncode pnMessageData) $ \ntfData ->
|
||||
apn apnMutableContent . Just $ J.object ["nonce" .= nonce, "message" .= ntfData]
|
||||
PNAlert text -> Right $ apn (apnAlert $ APNSAlertText text) Nothing
|
||||
PNCheckMessages -> Right $ apn APNSBackground {contentAvailable = 1} . Just $ J.object ["checkMessages" .= True]
|
||||
where
|
||||
encrypt :: ByteString -> (Text -> APNSNotification) -> Either C.CryptoError APNSNotification
|
||||
encrypt ntfData f = f . safeDecodeUtf8 . U.encode <$> C.cbEncrypt tknDhSecret nonce ntfData paddedLen
|
||||
apn aps notificationData = APNSNotification {aps, notificationData}
|
||||
apnMutableContent = APNSMutableContent {mutableContent = 1, alert = APNSAlertText "Encrypted message or some other app event", category = Nothing}
|
||||
apnMutableContent = APNSMutableContent {mutableContent = 1, alert = APNSAlertText "Encrypted message or another app event", category = Just ntfCategoryCheckMessage}
|
||||
apnAlert alert = APNSAlert {alert, badge = Nothing, sound = Nothing, category = Nothing}
|
||||
safeDecodeUtf8 = decodeUtf8With onError where onError _ _ = Just '?'
|
||||
|
||||
@@ -298,7 +321,7 @@ data PushProviderError
|
||||
| PPTokenInvalid
|
||||
| PPRetryLater
|
||||
| PPPermanentError
|
||||
deriving (Show)
|
||||
deriving (Show, Exception)
|
||||
|
||||
type PushProviderClient = NtfTknData -> PushNotification -> ExceptT PushProviderError IO ()
|
||||
|
||||
@@ -307,7 +330,7 @@ data APNSErrorResponse = APNSErrorResponse {reason :: Text}
|
||||
deriving (Generic, FromJSON)
|
||||
|
||||
apnsPushProviderClient :: APNSPushClient -> PushProviderClient
|
||||
apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {token = DeviceToken PPApns tknStr} pn = do
|
||||
apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {token = DeviceToken _ tknStr} pn = do
|
||||
http2 <- liftHTTPS2 $ getApnsHTTP2Client c
|
||||
nonce <- atomically $ C.pseudoRandomCbNonce nonceDrg
|
||||
apnsNtf <- liftEither $ first PPCryptoError $ apnsNotification tkn nonce (paddedNtfLength apnsCfg) pn
|
||||
|
||||
@@ -6,30 +6,42 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Store where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (catMaybes)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Word (Word16)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Protocol (NtfPrivateSignKey)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (whenM, ($>>=))
|
||||
|
||||
data NtfStore = NtfStore
|
||||
{ tokens :: TMap NtfTokenId NtfTknData,
|
||||
tokenRegistrations :: TMap DeviceToken (TMap ByteString NtfTokenId)
|
||||
-- multiple registrations exist to protect from malicious registrations if token is compromised
|
||||
tokenRegistrations :: TMap DeviceToken (TMap ByteString NtfTokenId),
|
||||
subscriptions :: TMap NtfSubscriptionId NtfSubData,
|
||||
tokenSubscriptions :: TMap NtfTokenId (TVar (Set NtfSubscriptionId)),
|
||||
subscriptionLookup :: TMap SMPQueueNtf NtfSubscriptionId
|
||||
}
|
||||
|
||||
newNtfStore :: STM NtfStore
|
||||
newNtfStore = do
|
||||
tokens <- TM.empty
|
||||
tokenRegistrations <- TM.empty
|
||||
pure NtfStore {tokens, tokenRegistrations}
|
||||
subscriptions <- TM.empty
|
||||
tokenSubscriptions <- TM.empty
|
||||
subscriptionLookup <- TM.empty
|
||||
pure NtfStore {tokens, tokenRegistrations, subscriptions, tokenSubscriptions, subscriptionLookup}
|
||||
|
||||
data NtfTknData = NtfTknData
|
||||
{ ntfTknId :: NtfTokenId,
|
||||
@@ -38,26 +50,20 @@ data NtfTknData = NtfTknData
|
||||
tknVerifyKey :: C.APublicVerifyKey,
|
||||
tknDhKeys :: C.KeyPair 'C.X25519,
|
||||
tknDhSecret :: C.DhSecretX25519,
|
||||
tknRegCode :: NtfRegCode
|
||||
tknRegCode :: NtfRegCode,
|
||||
tknCronInterval :: TVar Word16
|
||||
}
|
||||
|
||||
mkNtfTknData :: NtfTokenId -> NewNtfEntity 'Token -> C.KeyPair 'C.X25519 -> C.DhSecretX25519 -> NtfRegCode -> STM NtfTknData
|
||||
mkNtfTknData ntfTknId (NewNtfTkn token tknVerifyKey _) tknDhKeys tknDhSecret tknRegCode = do
|
||||
tknStatus <- newTVar NTRegistered
|
||||
pure NtfTknData {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode}
|
||||
|
||||
-- data NtfSubscriptionsStore = NtfSubscriptionsStore
|
||||
|
||||
-- { subscriptions :: TMap NtfSubsciptionId NtfSubsciption,
|
||||
-- activeSubscriptions :: TMap (SMPServer, NotifierId) NtfSubsciptionId
|
||||
-- }
|
||||
-- do
|
||||
-- subscriptions <- newTVar M.empty
|
||||
-- activeSubscriptions <- newTVar M.empty
|
||||
-- pure NtfSubscriptionsStore {subscriptions, activeSubscriptions}
|
||||
tknCronInterval <- newTVar 0
|
||||
pure NtfTknData {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval}
|
||||
|
||||
data NtfSubData = NtfSubData
|
||||
{ smpQueue :: SMPQueueNtf,
|
||||
{ ntfSubId :: NtfSubscriptionId,
|
||||
smpQueue :: SMPQueueNtf,
|
||||
notifierKey :: NtfPrivateSignKey,
|
||||
tokenId :: NtfTokenId,
|
||||
subStatus :: TVar NtfSubStatus
|
||||
}
|
||||
@@ -102,7 +108,16 @@ removeInactiveTokenRegistrations st NtfTknData {ntfTknId = tId, token} =
|
||||
TM.delete tId' $ tokens st
|
||||
pure $ map snd tIds
|
||||
|
||||
deleteNtfToken :: NtfStore -> NtfTokenId -> STM ()
|
||||
removeTokenRegistration :: NtfStore -> NtfTknData -> STM ()
|
||||
removeTokenRegistration st NtfTknData {ntfTknId = tId, token, tknVerifyKey} =
|
||||
TM.lookup token (tokenRegistrations st) >>= mapM_ removeReg
|
||||
where
|
||||
removeReg regs =
|
||||
TM.lookup k regs
|
||||
>>= mapM_ (\tId' -> when (tId == tId') $ TM.delete k regs)
|
||||
k = C.toPubKey C.pubKeyBytes tknVerifyKey
|
||||
|
||||
deleteNtfToken :: NtfStore -> NtfTokenId -> STM [SMPQueueNtf]
|
||||
deleteNtfToken st tknId = do
|
||||
TM.lookupDelete tknId (tokens st)
|
||||
>>= mapM_
|
||||
@@ -114,42 +129,72 @@ deleteNtfToken st tknId = do
|
||||
whenM (TM.null tIds) $ TM.delete token regs
|
||||
)
|
||||
)
|
||||
|
||||
qs <-
|
||||
TM.lookupDelete tknId (tokenSubscriptions st)
|
||||
>>= mapM
|
||||
( readTVar
|
||||
>=> mapM
|
||||
( \subId -> do
|
||||
TM.lookupDelete subId (subscriptions st)
|
||||
>>= mapM
|
||||
( \NtfSubData {smpQueue} ->
|
||||
TM.delete smpQueue (subscriptionLookup st) $> smpQueue
|
||||
)
|
||||
)
|
||||
. S.toList
|
||||
)
|
||||
pure $ maybe [] catMaybes qs
|
||||
where
|
||||
regs = tokenRegistrations st
|
||||
regKey = C.toPubKey C.pubKeyBytes
|
||||
|
||||
-- getNtfRec :: NtfStore -> SNtfEntity e -> NtfEntityId -> STM (Maybe (NtfEntityRec e))
|
||||
-- getNtfRec st ent entId = case ent of
|
||||
-- SToken -> NtfTkn <$$> TM.lookup entId (tokens st)
|
||||
-- SSubscription -> pure Nothing
|
||||
getNtfSubscription :: NtfStore -> NtfSubscriptionId -> STM (Maybe NtfSubData)
|
||||
getNtfSubscription st subId =
|
||||
TM.lookup subId (subscriptions st)
|
||||
|
||||
-- getNtfVerifyKey :: NtfStore -> SNtfEntity e -> NtfEntityId -> STM (Maybe (NtfEntityRec e, C.APublicVerifyKey))
|
||||
-- getNtfVerifyKey st ent entId =
|
||||
-- getNtfRec st ent entId >>= \case
|
||||
-- Just r@(NtfTkn NtfTknData {tknVerifyKey}) -> pure $ Just (r, tknVerifyKey)
|
||||
-- Just r@(NtfSub NtfSubData {tokenId}) ->
|
||||
-- getNtfRec st SToken tokenId >>= \case
|
||||
-- Just (NtfTkn NtfTknData {tknVerifyKey}) -> pure $ Just (r, tknVerifyKey)
|
||||
-- _ -> pure Nothing
|
||||
-- _ -> pure Nothing
|
||||
findNtfSubscription :: NtfStore -> SMPQueueNtf -> STM (Maybe NtfSubData)
|
||||
findNtfSubscription st smpQueue = do
|
||||
TM.lookup smpQueue (subscriptionLookup st)
|
||||
$>>= \subId -> TM.lookup subId (subscriptions st)
|
||||
|
||||
-- mkNtfSubsciption :: SMPQueueNtf -> NtfTokenId -> STM NtfSubsciption
|
||||
-- mkNtfSubsciption smpQueue tokenId = do
|
||||
-- subStatus <- newTVar NSNew
|
||||
-- pure NtfSubsciption {smpQueue, tokenId, subStatus}
|
||||
findNtfSubscriptionToken :: NtfStore -> SMPQueueNtf -> STM (Maybe NtfTknData)
|
||||
findNtfSubscriptionToken st smpQueue = do
|
||||
findNtfSubscription st smpQueue
|
||||
$>>= \NtfSubData {tokenId} -> getActiveNtfToken st tokenId
|
||||
|
||||
-- getNtfSub :: NtfSubscriptionsStore -> NtfSubsciptionId -> STM (Maybe NtfSubsciption)
|
||||
-- getNtfSub st subId = pure Nothing -- maybe (pure $ Left AUTH) (fmap Right . readTVar) . M.lookup subId . subscriptions =<< readTVar st
|
||||
getActiveNtfToken :: NtfStore -> NtfTokenId -> STM (Maybe NtfTknData)
|
||||
getActiveNtfToken st tknId =
|
||||
getNtfToken st tknId $>>= \tkn@NtfTknData {tknStatus} -> do
|
||||
tStatus <- readTVar tknStatus
|
||||
pure $ if tStatus == NTActive then Just tkn else Nothing
|
||||
|
||||
-- getNtfSubViaSMPQueue :: NtfSubscriptionsStore -> SMPQueueNtf -> STM (Maybe NtfSubsciption)
|
||||
-- getNtfSubViaSMPQueue st smpQueue = pure Nothing
|
||||
mkNtfSubData :: NtfSubscriptionId -> NewNtfEntity 'Subscription -> STM NtfSubData
|
||||
mkNtfSubData ntfSubId (NewNtfSub tokenId smpQueue notifierKey) = do
|
||||
subStatus <- newTVar NSNew
|
||||
pure NtfSubData {ntfSubId, smpQueue, tokenId, subStatus, notifierKey}
|
||||
|
||||
-- -- replace keeping status
|
||||
-- updateNtfSub :: NtfSubscriptionsStore -> NtfSubsciption -> SMPQueueNtf -> NtfTokenId -> C.DhSecretX25519 -> STM (Maybe ())
|
||||
-- updateNtfSub st sub smpQueue tokenId dhSecret = pure Nothing
|
||||
addNtfSubscription :: NtfStore -> NtfSubscriptionId -> NtfSubData -> STM (Maybe ())
|
||||
addNtfSubscription st subId sub@NtfSubData {smpQueue, tokenId} =
|
||||
TM.lookup tokenId (tokenSubscriptions st) >>= maybe newTokenSub pure >>= insertSub
|
||||
where
|
||||
newTokenSub = do
|
||||
ts <- newTVar S.empty
|
||||
TM.insert tokenId ts $ tokenSubscriptions st
|
||||
pure ts
|
||||
insertSub ts = do
|
||||
modifyTVar' ts $ S.insert subId
|
||||
TM.insert subId sub $ subscriptions st
|
||||
TM.insert smpQueue subId (subscriptionLookup st)
|
||||
-- return Nothing if subscription existed before
|
||||
pure $ Just ()
|
||||
|
||||
-- addNtfSub :: NtfSubscriptionsStore -> NtfSubsciptionId -> NtfSubsciption -> STM (Maybe ())
|
||||
-- addNtfSub st subId sub = pure Nothing
|
||||
|
||||
-- deleteNtfSub :: NtfSubscriptionsStore -> NtfSubsciptionId -> STM ()
|
||||
-- deleteNtfSub st subId = pure ()
|
||||
deleteNtfSubscription :: NtfStore -> NtfSubscriptionId -> STM ()
|
||||
deleteNtfSubscription st subId = do
|
||||
TM.lookupDelete subId (subscriptions st)
|
||||
>>= mapM_
|
||||
( \NtfSubData {smpQueue, tokenId} -> do
|
||||
TM.delete smpQueue $ subscriptionLookup st
|
||||
ts_ <- TM.lookup tokenId (tokenSubscriptions st)
|
||||
forM_ ts_ $ \ts -> modifyTVar' ts $ S.delete subId
|
||||
)
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.StoreLog
|
||||
( StoreLog,
|
||||
NtfStoreLogRecord (..),
|
||||
readWriteNtfStore,
|
||||
logCreateToken,
|
||||
logTokenStatus,
|
||||
logUpdateToken,
|
||||
logTokenCron,
|
||||
logDeleteToken,
|
||||
logCreateSubscription,
|
||||
logSubscriptionStatus,
|
||||
logDeleteSubscription,
|
||||
closeStoreLog,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (void)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Word (Word16)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Store
|
||||
import Simplex.Messaging.Protocol (NtfPrivateSignKey)
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (whenM)
|
||||
import System.Directory (doesFileExist, renameFile)
|
||||
import System.IO
|
||||
|
||||
data NtfStoreLogRecord
|
||||
= CreateToken NtfTknRec
|
||||
| TokenStatus NtfTokenId NtfTknStatus
|
||||
| UpdateToken NtfTokenId DeviceToken NtfRegCode
|
||||
| TokenCron NtfTokenId Word16
|
||||
| DeleteToken NtfTokenId
|
||||
| CreateSubscription NtfSubRec
|
||||
| SubscriptionStatus NtfSubscriptionId NtfSubStatus
|
||||
| DeleteSubscription NtfSubscriptionId
|
||||
|
||||
data NtfTknRec = NtfTknRec
|
||||
{ ntfTknId :: NtfTokenId,
|
||||
token :: DeviceToken,
|
||||
tknStatus :: NtfTknStatus,
|
||||
tknVerifyKey :: C.APublicVerifyKey,
|
||||
tknDhKeys :: C.KeyPair 'C.X25519,
|
||||
tknDhSecret :: C.DhSecretX25519,
|
||||
tknRegCode :: NtfRegCode,
|
||||
tknCronInterval :: Word16
|
||||
}
|
||||
|
||||
mkTknData :: NtfTknRec -> STM NtfTknData
|
||||
mkTknData NtfTknRec {ntfTknId, token, tknStatus = status, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval = cronInt} = do
|
||||
tknStatus <- newTVar status
|
||||
tknCronInterval <- newTVar cronInt
|
||||
pure NtfTknData {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval}
|
||||
|
||||
mkTknRec :: NtfTknData -> STM NtfTknRec
|
||||
mkTknRec NtfTknData {ntfTknId, token, tknStatus = status, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval = cronInt} = do
|
||||
tknStatus <- readTVar status
|
||||
tknCronInterval <- readTVar cronInt
|
||||
pure NtfTknRec {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval}
|
||||
|
||||
data NtfSubRec = NtfSubRec
|
||||
{ ntfSubId :: NtfSubscriptionId,
|
||||
smpQueue :: SMPQueueNtf,
|
||||
notifierKey :: NtfPrivateSignKey,
|
||||
tokenId :: NtfTokenId,
|
||||
subStatus :: NtfSubStatus
|
||||
}
|
||||
|
||||
mkSubData :: NtfSubRec -> STM NtfSubData
|
||||
mkSubData NtfSubRec {ntfSubId, smpQueue, notifierKey, tokenId, subStatus = status} = do
|
||||
subStatus <- newTVar status
|
||||
pure NtfSubData {ntfSubId, smpQueue, notifierKey, tokenId, subStatus}
|
||||
|
||||
mkSubRec :: NtfSubData -> STM NtfSubRec
|
||||
mkSubRec NtfSubData {ntfSubId, smpQueue, notifierKey, tokenId, subStatus = status} = do
|
||||
subStatus <- readTVar status
|
||||
pure NtfSubRec {ntfSubId, smpQueue, notifierKey, tokenId, subStatus}
|
||||
|
||||
instance StrEncoding NtfStoreLogRecord where
|
||||
strEncode = \case
|
||||
CreateToken tknRec -> strEncode (Str "TCREATE", tknRec)
|
||||
TokenStatus tknId tknStatus -> strEncode (Str "TSTATUS", tknId, tknStatus)
|
||||
UpdateToken tknId token regCode -> strEncode (Str "TUPDATE", tknId, token, regCode)
|
||||
TokenCron tknId cronInt -> strEncode (Str "TCRON", tknId, cronInt)
|
||||
DeleteToken tknId -> strEncode (Str "TDELETE", tknId)
|
||||
CreateSubscription subRec -> strEncode (Str "SCREATE", subRec)
|
||||
SubscriptionStatus subId subStatus -> strEncode (Str "SSTATUS", subId, subStatus)
|
||||
DeleteSubscription subId -> strEncode (Str "SDELETE", subId)
|
||||
strP =
|
||||
A.choice
|
||||
[ "TCREATE " *> (CreateToken <$> strP),
|
||||
"TSTATUS " *> (TokenStatus <$> strP_ <*> strP),
|
||||
"TUPDATE " *> (UpdateToken <$> strP_ <*> strP_ <*> strP),
|
||||
"TCRON " *> (TokenCron <$> strP_ <*> strP),
|
||||
"TDELETE " *> (DeleteToken <$> strP),
|
||||
"SCREATE " *> (CreateSubscription <$> strP),
|
||||
"SSTATUS " *> (SubscriptionStatus <$> strP_ <*> strP),
|
||||
"SDELETE " *> (DeleteSubscription <$> strP)
|
||||
]
|
||||
|
||||
instance StrEncoding NtfTknRec where
|
||||
strEncode NtfTknRec {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval} =
|
||||
B.unwords
|
||||
[ "tknId=" <> strEncode ntfTknId,
|
||||
"token=" <> strEncode token,
|
||||
"tokenStatus=" <> strEncode tknStatus,
|
||||
"verifyKey=" <> strEncode tknVerifyKey,
|
||||
"dhKeys=" <> strEncode tknDhKeys,
|
||||
"dhSecret=" <> strEncode tknDhSecret,
|
||||
"regCode=" <> strEncode tknRegCode,
|
||||
"cron=" <> strEncode tknCronInterval
|
||||
]
|
||||
strP = do
|
||||
ntfTknId <- "tknId=" *> strP_
|
||||
token <- "token=" *> strP_
|
||||
tknStatus <- "tokenStatus=" *> strP_
|
||||
tknVerifyKey <- "verifyKey=" *> strP_
|
||||
tknDhKeys <- "dhKeys=" *> strP_
|
||||
tknDhSecret <- "dhSecret=" *> strP_
|
||||
tknRegCode <- "regCode=" *> strP_
|
||||
tknCronInterval <- "cron=" *> strP
|
||||
pure NtfTknRec {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval}
|
||||
|
||||
instance StrEncoding NtfSubRec where
|
||||
strEncode NtfSubRec {ntfSubId, smpQueue, notifierKey, tokenId, subStatus} =
|
||||
B.unwords
|
||||
[ "subId=" <> strEncode ntfSubId,
|
||||
"smpQueue=" <> strEncode smpQueue,
|
||||
"notifierKey=" <> strEncode notifierKey,
|
||||
"tknId=" <> strEncode tokenId,
|
||||
"subStatus=" <> strEncode subStatus
|
||||
]
|
||||
strP = do
|
||||
ntfSubId <- "subId=" *> strP_
|
||||
smpQueue <- "smpQueue=" *> strP_
|
||||
notifierKey <- "notifierKey=" *> strP_
|
||||
tokenId <- "tknId=" *> strP_
|
||||
subStatus <- "subStatus=" *> strP
|
||||
pure NtfSubRec {ntfSubId, smpQueue, notifierKey, tokenId, subStatus}
|
||||
|
||||
logNtfStoreRecord :: StoreLog 'WriteMode -> NtfStoreLogRecord -> IO ()
|
||||
logNtfStoreRecord = writeStoreLogRecord
|
||||
|
||||
logCreateToken :: StoreLog 'WriteMode -> NtfTknData -> IO ()
|
||||
logCreateToken s tkn = logNtfStoreRecord s . CreateToken =<< atomically (mkTknRec tkn)
|
||||
|
||||
logTokenStatus :: StoreLog 'WriteMode -> NtfTokenId -> NtfTknStatus -> IO ()
|
||||
logTokenStatus s tknId tknStatus = logNtfStoreRecord s $ TokenStatus tknId tknStatus
|
||||
|
||||
logUpdateToken :: StoreLog 'WriteMode -> NtfTokenId -> DeviceToken -> NtfRegCode -> IO ()
|
||||
logUpdateToken s tknId token regCode = logNtfStoreRecord s $ UpdateToken tknId token regCode
|
||||
|
||||
logTokenCron :: StoreLog 'WriteMode -> NtfTokenId -> Word16 -> IO ()
|
||||
logTokenCron s tknId cronInt = logNtfStoreRecord s $ TokenCron tknId cronInt
|
||||
|
||||
logDeleteToken :: StoreLog 'WriteMode -> NtfTokenId -> IO ()
|
||||
logDeleteToken s tknId = logNtfStoreRecord s $ DeleteToken tknId
|
||||
|
||||
logCreateSubscription :: StoreLog 'WriteMode -> NtfSubData -> IO ()
|
||||
logCreateSubscription s sub = logNtfStoreRecord s . CreateSubscription =<< atomically (mkSubRec sub)
|
||||
|
||||
logSubscriptionStatus :: StoreLog 'WriteMode -> NtfSubscriptionId -> NtfSubStatus -> IO ()
|
||||
logSubscriptionStatus s subId subStatus = logNtfStoreRecord s $ SubscriptionStatus subId subStatus
|
||||
|
||||
logDeleteSubscription :: StoreLog 'WriteMode -> NtfSubscriptionId -> IO ()
|
||||
logDeleteSubscription s subId = logNtfStoreRecord s $ DeleteSubscription subId
|
||||
|
||||
readWriteNtfStore :: FilePath -> NtfStore -> IO (StoreLog 'WriteMode)
|
||||
readWriteNtfStore f st = do
|
||||
whenM (doesFileExist f) $ do
|
||||
readNtfStore f st
|
||||
renameFile f $ f <> ".bak"
|
||||
s <- openWriteStoreLog f
|
||||
writeNtfStore s st
|
||||
pure s
|
||||
|
||||
readNtfStore :: FilePath -> NtfStore -> IO ()
|
||||
readNtfStore f st = mapM_ addNtfLogRecord . B.lines =<< B.readFile f
|
||||
where
|
||||
addNtfLogRecord s = case strDecode s of
|
||||
Left e -> B.putStrLn $ "Log parsing error (" <> B.pack e <> "): " <> B.take 100 s
|
||||
Right lr -> atomically $ case lr of
|
||||
CreateToken r@NtfTknRec {ntfTknId} -> do
|
||||
tkn <- mkTknData r
|
||||
addNtfToken st ntfTknId tkn
|
||||
TokenStatus tknId status ->
|
||||
getNtfToken st tknId
|
||||
>>= mapM_ (\NtfTknData {tknStatus} -> writeTVar tknStatus status)
|
||||
UpdateToken tknId token' tknRegCode ->
|
||||
getNtfToken st tknId
|
||||
>>= mapM_
|
||||
( \tkn@NtfTknData {tknStatus} -> do
|
||||
removeTokenRegistration st tkn
|
||||
writeTVar tknStatus NTRegistered
|
||||
addNtfToken st tknId tkn {token = token', tknRegCode}
|
||||
)
|
||||
TokenCron tknId cronInt ->
|
||||
getNtfToken st tknId
|
||||
>>= mapM_ (\NtfTknData {tknCronInterval} -> writeTVar tknCronInterval cronInt)
|
||||
DeleteToken tknId ->
|
||||
void $ deleteNtfToken st tknId
|
||||
CreateSubscription r@NtfSubRec {ntfSubId} -> do
|
||||
sub <- mkSubData r
|
||||
void $ addNtfSubscription st ntfSubId sub
|
||||
SubscriptionStatus subId status ->
|
||||
getNtfSubscription st subId
|
||||
>>= mapM_ (\NtfSubData {subStatus} -> writeTVar subStatus status)
|
||||
DeleteSubscription subId ->
|
||||
deleteNtfSubscription st subId
|
||||
|
||||
writeNtfStore :: StoreLog 'WriteMode -> NtfStore -> IO ()
|
||||
writeNtfStore s NtfStore {tokens, subscriptions} = do
|
||||
atomically (readTVar tokens >>= mapM mkTknRec)
|
||||
>>= mapM_ (writeStoreLogRecord s . CreateToken)
|
||||
atomically (readTVar subscriptions >>= mapM mkSubRec)
|
||||
>>= mapM_ (writeStoreLogRecord s . CreateSubscription)
|
||||
@@ -3,17 +3,21 @@ module Simplex.Messaging.Notifications.Transport where
|
||||
import Control.Monad.Except
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Version
|
||||
|
||||
ntfBlockSize :: Int
|
||||
ntfBlockSize = 512
|
||||
|
||||
supportedNTFServerVRange :: VersionRange
|
||||
supportedNTFServerVRange = mkVersionRange 1 1
|
||||
|
||||
-- | Notifcations server transport handshake.
|
||||
ntfServerHandshake :: Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
|
||||
ntfServerHandshake c _ = pure $ ntfTHandle c
|
||||
ntfServerHandshake :: Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
|
||||
ntfServerHandshake c _ _ = pure $ ntfTHandle c
|
||||
|
||||
-- | Notifcations server client transport handshake.
|
||||
ntfClientHandshake :: Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
|
||||
ntfClientHandshake c _ = pure $ ntfTHandle c
|
||||
ntfClientHandshake :: Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
|
||||
ntfClientHandshake c _ _ = pure $ ntfTHandle c
|
||||
|
||||
ntfTHandle :: Transport c => c -> THandle c
|
||||
ntfTHandle c = THandle {connection = c, sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = 0}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Types where
|
||||
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time (UTCTime)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, NotificationsMode (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder, fromTextField_)
|
||||
import Simplex.Messaging.Protocol (NotifierId, ProtocolServer, SMPServer)
|
||||
|
||||
type NtfServer = ProtocolServer
|
||||
|
||||
data NtfTknAction
|
||||
= NTARegister
|
||||
| NTAVerify NtfRegCode -- code to verify token
|
||||
| NTACheck
|
||||
| NTADelete
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding NtfTknAction where
|
||||
smpEncode = \case
|
||||
NTARegister -> "R"
|
||||
NTAVerify code -> smpEncode ('V', code)
|
||||
NTACheck -> "C"
|
||||
NTADelete -> "D"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'R' -> pure NTARegister
|
||||
'V' -> NTAVerify <$> smpP
|
||||
'C' -> pure NTACheck
|
||||
'D' -> pure NTADelete
|
||||
_ -> fail "bad NtfTknAction"
|
||||
|
||||
instance FromField NtfTknAction where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
instance ToField NtfTknAction where toField = toField . smpEncode
|
||||
|
||||
data NtfToken = NtfToken
|
||||
{ deviceToken :: DeviceToken,
|
||||
ntfServer :: NtfServer,
|
||||
ntfTokenId :: Maybe NtfTokenId,
|
||||
-- | key used by the ntf server to verify transmissions
|
||||
ntfPubKey :: C.APublicVerifyKey,
|
||||
-- | key used by the ntf client to sign transmissions
|
||||
ntfPrivKey :: C.APrivateSignKey,
|
||||
-- | client's DH keys (to repeat registration if necessary)
|
||||
ntfDhKeys :: C.KeyPair 'C.X25519,
|
||||
-- | shared DH secret used to encrypt/decrypt notifications e2e
|
||||
ntfDhSecret :: Maybe C.DhSecretX25519,
|
||||
-- | token status
|
||||
ntfTknStatus :: NtfTknStatus,
|
||||
-- | pending token action and the earliest time
|
||||
ntfTknAction :: Maybe NtfTknAction,
|
||||
ntfMode :: NotificationsMode
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newNtfToken :: DeviceToken -> NtfServer -> C.ASignatureKeyPair -> C.KeyPair 'C.X25519 -> NotificationsMode -> NtfToken
|
||||
newNtfToken deviceToken ntfServer (ntfPubKey, ntfPrivKey) ntfDhKeys ntfMode =
|
||||
NtfToken
|
||||
{ deviceToken,
|
||||
ntfServer,
|
||||
ntfTokenId = Nothing,
|
||||
ntfPubKey,
|
||||
ntfPrivKey,
|
||||
ntfDhKeys,
|
||||
ntfDhSecret = Nothing,
|
||||
ntfTknStatus = NTNew,
|
||||
ntfTknAction = Just NTARegister,
|
||||
ntfMode
|
||||
}
|
||||
|
||||
data NtfSubAction = NtfSubNTFAction NtfSubNTFAction | NtfSubSMPAction NtfSubSMPAction
|
||||
deriving (Show)
|
||||
|
||||
isDeleteNtfSubAction :: NtfSubAction -> Bool
|
||||
isDeleteNtfSubAction = \case
|
||||
NtfSubNTFAction a -> case a of
|
||||
NSACreate -> False
|
||||
NSACheck -> False
|
||||
NSADelete -> True
|
||||
NtfSubSMPAction a -> case a of
|
||||
NSASmpKey -> False
|
||||
NSASmpDelete -> True
|
||||
|
||||
type NtfActionTs = UTCTime
|
||||
|
||||
data NtfSubNTFAction
|
||||
= NSACreate
|
||||
| NSACheck
|
||||
| NSADelete
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding NtfSubNTFAction where
|
||||
smpEncode = \case
|
||||
NSACreate -> "N"
|
||||
NSACheck -> "C"
|
||||
NSADelete -> "D"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'N' -> pure NSACreate
|
||||
'C' -> pure NSACheck
|
||||
'D' -> pure NSADelete
|
||||
_ -> fail "bad NtfSubNTFAction"
|
||||
|
||||
instance FromField NtfSubNTFAction where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
instance ToField NtfSubNTFAction where toField = toField . smpEncode
|
||||
|
||||
data NtfSubSMPAction
|
||||
= NSASmpKey
|
||||
| NSASmpDelete
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding NtfSubSMPAction where
|
||||
smpEncode = \case
|
||||
NSASmpKey -> "K"
|
||||
NSASmpDelete -> "D"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'K' -> pure NSASmpKey
|
||||
'D' -> pure NSASmpDelete
|
||||
_ -> fail "bad NtfSubSMPAction"
|
||||
|
||||
instance FromField NtfSubSMPAction where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
instance ToField NtfSubSMPAction where toField = toField . smpEncode
|
||||
|
||||
data NtfAgentSubStatus
|
||||
= -- | subscription started
|
||||
NASNew
|
||||
| -- | state after NKEY - notifier ID is assigned to queue on SMP server
|
||||
NASKey
|
||||
| -- | state after SNEW - subscription created on notification server
|
||||
NASCreated NtfSubStatus
|
||||
| -- | state after SDEL (subscription is deleted on notification server)
|
||||
NASOff
|
||||
| -- | state after NDEL (notifier credentials are deleted on SMP server)
|
||||
-- Can only exist transiently - if subscription record was updated by notification supervisor mid worker operation,
|
||||
-- and hence got updated instead of being fully deleted in the database post operation by worker
|
||||
NASDeleted
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Encoding NtfAgentSubStatus where
|
||||
smpEncode = \case
|
||||
NASNew -> "NEW"
|
||||
NASKey -> "KEY"
|
||||
NASCreated status -> "CREATED " <> smpEncode status
|
||||
NASOff -> "OFF"
|
||||
NASDeleted -> "DELETED"
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"NEW" -> pure NASNew
|
||||
"KEY" -> pure NASKey
|
||||
"CREATED" -> do
|
||||
_ <- A.space
|
||||
NASCreated <$> smpP
|
||||
"OFF" -> pure NASOff
|
||||
"DELETED" -> pure NASDeleted
|
||||
_ -> fail "bad NtfAgentSubStatus"
|
||||
|
||||
instance FromField NtfAgentSubStatus where fromField = fromTextField_ $ either (const Nothing) Just . smpDecode . encodeUtf8
|
||||
|
||||
instance ToField NtfAgentSubStatus where toField = toField . decodeLatin1 . smpEncode
|
||||
|
||||
data NtfSubscription = NtfSubscription
|
||||
{ connId :: ConnId,
|
||||
smpServer :: SMPServer,
|
||||
ntfQueueId :: Maybe NotifierId,
|
||||
ntfServer :: NtfServer,
|
||||
ntfSubId :: Maybe NtfSubscriptionId,
|
||||
ntfSubStatus :: NtfAgentSubStatus
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newNtfSubscription :: ConnId -> SMPServer -> Maybe NotifierId -> NtfServer -> NtfAgentSubStatus -> NtfSubscription
|
||||
newNtfSubscription connId smpServer ntfQueueId ntfServer ntfSubStatus =
|
||||
NtfSubscription
|
||||
{ connId,
|
||||
smpServer,
|
||||
ntfQueueId,
|
||||
ntfServer,
|
||||
ntfSubId = Nothing,
|
||||
ntfSubStatus
|
||||
}
|
||||
@@ -76,8 +76,15 @@ module Simplex.Messaging.Protocol
|
||||
SndPublicVerifyKey,
|
||||
NtfPrivateSignKey,
|
||||
NtfPublicVerifyKey,
|
||||
RcvNtfPublicDhKey,
|
||||
RcvNtfDhSecret,
|
||||
MsgId,
|
||||
MsgBody,
|
||||
EncNMsgMeta,
|
||||
SMPMsgMeta (..),
|
||||
NMsgMeta (..),
|
||||
MsgFlags (..),
|
||||
noMsgFlags,
|
||||
|
||||
-- * Parse and serialize
|
||||
ProtocolMsgTag (..),
|
||||
@@ -213,12 +220,18 @@ data Command (p :: Party) where
|
||||
NEW :: RcvPublicVerifyKey -> RcvPublicDhKey -> Command Recipient
|
||||
SUB :: Command Recipient
|
||||
KEY :: SndPublicVerifyKey -> Command Recipient
|
||||
NKEY :: NtfPublicVerifyKey -> Command Recipient
|
||||
ACK :: Command Recipient
|
||||
NKEY :: NtfPublicVerifyKey -> RcvNtfPublicDhKey -> Command Recipient
|
||||
NDEL :: Command Recipient
|
||||
GET :: Command Recipient
|
||||
-- ACK v1 has to be supported for encoding/decoding
|
||||
-- ACK :: Command Recipient
|
||||
ACK :: MsgId -> Command Recipient
|
||||
OFF :: Command Recipient
|
||||
DEL :: Command Recipient
|
||||
-- SMP sender commands
|
||||
SEND :: MsgBody -> Command Sender
|
||||
-- SEND v1 has to be supported for encoding/decoding
|
||||
-- SEND :: MsgBody -> Command Sender
|
||||
SEND :: MsgFlags -> MsgBody -> Command Sender
|
||||
PING :: Command Sender
|
||||
-- SMP notification subscriber commands
|
||||
NSUB :: Command Notifier
|
||||
@@ -230,15 +243,60 @@ deriving instance Eq (Command p)
|
||||
data BrokerMsg where
|
||||
-- SMP broker messages (responses, client messages, notifications)
|
||||
IDS :: QueueIdsKeys -> BrokerMsg
|
||||
MSG :: MsgId -> SystemTime -> MsgBody -> BrokerMsg
|
||||
NID :: NotifierId -> BrokerMsg
|
||||
NMSG :: BrokerMsg
|
||||
-- MSG v1 has to be supported for encoding/decoding
|
||||
-- MSG :: MsgId -> SystemTime -> MsgBody -> BrokerMsg
|
||||
MSG :: MsgId -> SystemTime -> MsgFlags -> MsgBody -> BrokerMsg
|
||||
NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg
|
||||
NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg
|
||||
END :: BrokerMsg
|
||||
OK :: BrokerMsg
|
||||
ERR :: ErrorType -> BrokerMsg
|
||||
PONG :: BrokerMsg
|
||||
deriving (Eq, Show)
|
||||
|
||||
type EncNMsgMeta = ByteString
|
||||
|
||||
data SMPMsgMeta = SMPMsgMeta
|
||||
{ msgId :: MsgId,
|
||||
msgTs :: SystemTime,
|
||||
msgFlags :: MsgFlags
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data NMsgMeta = NMsgMeta
|
||||
{ msgId :: MsgId,
|
||||
msgTs :: SystemTime
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding NMsgMeta where
|
||||
smpEncode NMsgMeta {msgId, msgTs} =
|
||||
smpEncode (msgId, msgTs)
|
||||
smpP = do
|
||||
-- Tail here is to allow extension in the future clients/servers
|
||||
(msgId, msgTs, Tail _) <- smpP
|
||||
pure NMsgMeta {msgId, msgTs}
|
||||
|
||||
data MsgFlags = MsgFlags {notification :: Bool}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON MsgFlags where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
instance Encoding MsgFlags where
|
||||
smpEncode MsgFlags {notification} = smpEncode notification
|
||||
smpP = do
|
||||
notification <- smpP <* A.takeTill (== ' ')
|
||||
pure MsgFlags {notification}
|
||||
|
||||
instance StrEncoding MsgFlags where
|
||||
strEncode = smpEncode
|
||||
{-# INLINE strEncode #-}
|
||||
strP = smpP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
noMsgFlags :: MsgFlags
|
||||
noMsgFlags = MsgFlags {notification = False}
|
||||
|
||||
-- * SMP command tags
|
||||
|
||||
data CommandTag (p :: Party) where
|
||||
@@ -246,6 +304,8 @@ data CommandTag (p :: Party) where
|
||||
SUB_ :: CommandTag Recipient
|
||||
KEY_ :: CommandTag Recipient
|
||||
NKEY_ :: CommandTag Recipient
|
||||
NDEL_ :: CommandTag Recipient
|
||||
GET_ :: CommandTag Recipient
|
||||
ACK_ :: CommandTag Recipient
|
||||
OFF_ :: CommandTag Recipient
|
||||
DEL_ :: CommandTag Recipient
|
||||
@@ -284,6 +344,8 @@ instance PartyI p => Encoding (CommandTag p) where
|
||||
SUB_ -> "SUB"
|
||||
KEY_ -> "KEY"
|
||||
NKEY_ -> "NKEY"
|
||||
NDEL_ -> "NDEL"
|
||||
GET_ -> "GET"
|
||||
ACK_ -> "ACK"
|
||||
OFF_ -> "OFF"
|
||||
DEL_ -> "DEL"
|
||||
@@ -298,6 +360,8 @@ instance ProtocolMsgTag CmdTag where
|
||||
"SUB" -> Just $ CT SRecipient SUB_
|
||||
"KEY" -> Just $ CT SRecipient KEY_
|
||||
"NKEY" -> Just $ CT SRecipient NKEY_
|
||||
"NDEL" -> Just $ CT SRecipient NDEL_
|
||||
"GET" -> Just $ CT SRecipient GET_
|
||||
"ACK" -> Just $ CT SRecipient ACK_
|
||||
"OFF" -> Just $ CT SRecipient OFF_
|
||||
"DEL" -> Just $ CT SRecipient DEL_
|
||||
@@ -476,12 +540,18 @@ type SndPrivateSignKey = C.APrivateSignKey
|
||||
-- | Sender's public key used by SMP server to verify authorization of SMP commands.
|
||||
type SndPublicVerifyKey = C.APublicVerifyKey
|
||||
|
||||
-- | Private key used by push notifications server to authorize (sign) LSTN command.
|
||||
-- | Private key used by push notifications server to authorize (sign) NSUB command.
|
||||
type NtfPrivateSignKey = C.APrivateSignKey
|
||||
|
||||
-- | Public key used by SMP server to verify authorization of LSTN command sent by push notifications server.
|
||||
-- | Public key used by SMP server to verify authorization of NSUB command sent by push notifications server.
|
||||
type NtfPublicVerifyKey = C.APublicVerifyKey
|
||||
|
||||
-- | Public key used for DH exchange to encrypt notification metadata from server to recipient
|
||||
type RcvNtfPublicDhKey = C.PublicKeyX25519
|
||||
|
||||
-- | DH Secret used to encrypt notification metadata from server to recipient
|
||||
type RcvNtfDhSecret = C.DhSecretX25519
|
||||
|
||||
-- | SMP message server ID.
|
||||
type MsgId = ByteString
|
||||
|
||||
@@ -526,6 +596,8 @@ data CommandError
|
||||
UNKNOWN
|
||||
| -- | error parsing command
|
||||
SYNTAX
|
||||
| -- | command is not allowed (SUB/GET cannot be used with the same queue in the same TCP connection)
|
||||
PROHIBITED
|
||||
| -- | transmission has no required credentials (signature or queue ID)
|
||||
NO_AUTH
|
||||
| -- | transmission has credentials that are not allowed for this command
|
||||
@@ -556,9 +628,9 @@ transmissionP = do
|
||||
command <- A.takeByteString
|
||||
pure RawTransmission {signature, signed, sessId, corrId, entityId, command}
|
||||
|
||||
class (ProtocolEncoding msg, ProtocolEncoding (ProtocolCommand msg)) => Protocol msg where
|
||||
class (ProtocolEncoding msg, ProtocolEncoding (ProtocolCommand msg), Show msg) => Protocol msg where
|
||||
type ProtocolCommand msg = cmd | cmd -> msg
|
||||
protocolClientHandshake :: forall c. Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
|
||||
protocolClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
|
||||
protocolPing :: ProtocolCommand msg
|
||||
protocolError :: msg -> Maybe ErrorType
|
||||
|
||||
@@ -572,28 +644,34 @@ instance Protocol BrokerMsg where
|
||||
|
||||
class ProtocolMsgTag (Tag msg) => ProtocolEncoding msg where
|
||||
type Tag msg
|
||||
encodeProtocol :: msg -> ByteString
|
||||
protocolP :: Tag msg -> Parser msg
|
||||
encodeProtocol :: Version -> msg -> ByteString
|
||||
protocolP :: Version -> Tag msg -> Parser msg
|
||||
checkCredentials :: SignedRawTransmission -> msg -> Either ErrorType msg
|
||||
|
||||
instance PartyI p => ProtocolEncoding (Command p) where
|
||||
type Tag (Command p) = CommandTag p
|
||||
encodeProtocol = \case
|
||||
encodeProtocol v = \case
|
||||
NEW rKey dhKey -> e (NEW_, ' ', rKey, dhKey)
|
||||
SUB -> e SUB_
|
||||
KEY k -> e (KEY_, ' ', k)
|
||||
NKEY k -> e (NKEY_, ' ', k)
|
||||
ACK -> e ACK_
|
||||
NKEY k dhKey -> e (NKEY_, ' ', k, dhKey)
|
||||
NDEL -> e NDEL_
|
||||
GET -> e GET_
|
||||
ACK msgId
|
||||
| v == 1 -> e ACK_
|
||||
| otherwise -> e (ACK_, ' ', msgId)
|
||||
OFF -> e OFF_
|
||||
DEL -> e DEL_
|
||||
SEND msg -> e (SEND_, ' ', Tail msg)
|
||||
SEND flags msg
|
||||
| v == 1 -> e (SEND_, ' ', Tail msg)
|
||||
| otherwise -> e (SEND_, ' ', flags, ' ', Tail msg)
|
||||
PING -> e PING_
|
||||
NSUB -> e NSUB_
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
|
||||
protocolP tag = (\(Cmd _ c) -> checkParty c) <$?> protocolP (CT (sParty @p) tag)
|
||||
protocolP v tag = (\(Cmd _ c) -> checkParty c) <$?> protocolP v (CT (sParty @p) tag)
|
||||
|
||||
checkCredentials (sig, _, queueId, _) cmd = case cmd of
|
||||
-- NEW must have signature but NOT queue ID
|
||||
@@ -602,7 +680,7 @@ instance PartyI p => ProtocolEncoding (Command p) where
|
||||
| not (B.null queueId) -> Left $ CMD HAS_AUTH
|
||||
| otherwise -> Right cmd
|
||||
-- SEND must have queue ID, signature is not always required
|
||||
SEND _
|
||||
SEND {}
|
||||
| B.null queueId -> Left $ CMD NO_ENTITY
|
||||
| otherwise -> Right cmd
|
||||
-- PING must not have queue ID or signature
|
||||
@@ -616,21 +694,27 @@ instance PartyI p => ProtocolEncoding (Command p) where
|
||||
|
||||
instance ProtocolEncoding Cmd where
|
||||
type Tag Cmd = CmdTag
|
||||
encodeProtocol (Cmd _ c) = encodeProtocol c
|
||||
encodeProtocol v (Cmd _ c) = encodeProtocol v c
|
||||
|
||||
protocolP = \case
|
||||
protocolP v = \case
|
||||
CT SRecipient tag ->
|
||||
Cmd SRecipient <$> case tag of
|
||||
NEW_ -> NEW <$> _smpP <*> smpP
|
||||
SUB_ -> pure SUB
|
||||
KEY_ -> KEY <$> _smpP
|
||||
NKEY_ -> NKEY <$> _smpP
|
||||
ACK_ -> pure ACK
|
||||
NKEY_ -> NKEY <$> _smpP <*> smpP
|
||||
NDEL_ -> pure NDEL
|
||||
GET_ -> pure GET
|
||||
ACK_
|
||||
| v == 1 -> pure $ ACK ""
|
||||
| otherwise -> ACK <$> _smpP
|
||||
OFF_ -> pure OFF
|
||||
DEL_ -> pure DEL
|
||||
CT SSender tag ->
|
||||
Cmd SSender <$> case tag of
|
||||
SEND_ -> SEND . unTail <$> _smpP
|
||||
SEND_
|
||||
| v == 1 -> SEND <$> pure noMsgFlags <*> (unTail <$> _smpP)
|
||||
| otherwise -> SEND <$> _smpP <*> (unTail <$> _smpP)
|
||||
PING_ -> pure PING
|
||||
CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB
|
||||
|
||||
@@ -638,11 +722,13 @@ instance ProtocolEncoding Cmd where
|
||||
|
||||
instance ProtocolEncoding BrokerMsg where
|
||||
type Tag BrokerMsg = BrokerMsgTag
|
||||
encodeProtocol = \case
|
||||
encodeProtocol v = \case
|
||||
IDS (QIK rcvId sndId srvDh) -> e (IDS_, ' ', rcvId, sndId, srvDh)
|
||||
MSG msgId ts msgBody -> e (MSG_, ' ', msgId, ts, Tail msgBody)
|
||||
NID nId -> e (NID_, ' ', nId)
|
||||
NMSG -> e NMSG_
|
||||
MSG msgId ts flags msgBody
|
||||
| v == 1 -> e (MSG_, ' ', msgId, ts, Tail msgBody)
|
||||
| otherwise -> e (MSG_, ' ', msgId, ts, flags, ' ', Tail msgBody)
|
||||
NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh)
|
||||
NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta)
|
||||
END -> e END_
|
||||
OK -> e OK_
|
||||
ERR err -> e (ERR_, ' ', err)
|
||||
@@ -651,11 +737,13 @@ instance ProtocolEncoding BrokerMsg where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
|
||||
protocolP = \case
|
||||
MSG_ -> MSG <$> _smpP <*> smpP <*> (unTail <$> smpP)
|
||||
protocolP v = \case
|
||||
MSG_
|
||||
| v == 1 -> MSG <$> _smpP <*> smpP <*> pure noMsgFlags <*> (unTail <$> smpP)
|
||||
| otherwise -> MSG <$> _smpP <*> smpP <*> smpP <*> (unTail <$> _smpP)
|
||||
IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP)
|
||||
NID_ -> NID <$> _smpP
|
||||
NMSG_ -> pure NMSG
|
||||
NID_ -> NID <$> _smpP <*> smpP
|
||||
NMSG_ -> NMSG <$> _smpP <*> smpP
|
||||
END_ -> pure END
|
||||
OK_ -> pure OK
|
||||
ERR_ -> ERR <$> _smpP
|
||||
@@ -679,11 +767,11 @@ _smpP :: Encoding a => Parser a
|
||||
_smpP = A.space *> smpP
|
||||
|
||||
-- | Parse SMP protocol commands and broker messages
|
||||
parseProtocol :: ProtocolEncoding msg => ByteString -> Either ErrorType msg
|
||||
parseProtocol s =
|
||||
parseProtocol :: ProtocolEncoding msg => Version -> ByteString -> Either ErrorType msg
|
||||
parseProtocol v s =
|
||||
let (tag, params) = B.break (== ' ') s
|
||||
in case decodeTag tag of
|
||||
Just cmd -> parse (protocolP cmd) (CMD SYNTAX) params
|
||||
Just cmd -> parse (protocolP v cmd) (CMD SYNTAX) params
|
||||
Nothing -> Left $ CMD UNKNOWN
|
||||
|
||||
checkParty :: forall t p p'. (PartyI p, PartyI p') => t p' -> Either String (t p)
|
||||
@@ -725,6 +813,7 @@ instance Encoding CommandError where
|
||||
smpEncode e = case e of
|
||||
UNKNOWN -> "UNKNOWN"
|
||||
SYNTAX -> "SYNTAX"
|
||||
PROHIBITED -> "PROHIBITED"
|
||||
NO_AUTH -> "NO_AUTH"
|
||||
HAS_AUTH -> "HAS_AUTH"
|
||||
NO_ENTITY -> "NO_ENTITY"
|
||||
@@ -732,6 +821,7 @@ instance Encoding CommandError where
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"UNKNOWN" -> pure UNKNOWN
|
||||
"SYNTAX" -> pure SYNTAX
|
||||
"PROHIBITED" -> pure PROHIBITED
|
||||
"NO_AUTH" -> pure NO_AUTH
|
||||
"HAS_AUTH" -> pure HAS_AUTH
|
||||
"NO_ENTITY" -> pure NO_ENTITY
|
||||
@@ -742,9 +832,9 @@ instance Encoding CommandError where
|
||||
tPut :: Transport c => THandle c -> SentRawTransmission -> IO (Either TransportError ())
|
||||
tPut th (sig, t) = tPutBlock th $ smpEncode (C.signatureBytes sig) <> t
|
||||
|
||||
encodeTransmission :: ProtocolEncoding c => ByteString -> Transmission c -> ByteString
|
||||
encodeTransmission sessionId (CorrId corrId, queueId, command) =
|
||||
smpEncode (sessionId, corrId, queueId) <> encodeProtocol command
|
||||
encodeTransmission :: ProtocolEncoding c => Version -> ByteString -> Transmission c -> ByteString
|
||||
encodeTransmission v sessionId (CorrId corrId, queueId, command) =
|
||||
smpEncode (sessionId, corrId, queueId) <> encodeProtocol v command
|
||||
|
||||
-- | Receive and parse transmission from the TCP transport (ignoring any trailing padding).
|
||||
tGetParse :: Transport c => THandle c -> IO (Either TransportError RawTransmission)
|
||||
@@ -752,7 +842,7 @@ tGetParse th = (parse transmissionP TEBadBlock =<<) <$> tGetBlock th
|
||||
|
||||
-- | Receive client and server transmissions (determined by `cmd` type).
|
||||
tGet :: forall cmd c m. (ProtocolEncoding cmd, Transport c, MonadIO m) => THandle c -> m (SignedTransmission cmd)
|
||||
tGet th@THandle {sessionId} = liftIO (tGetParse th) >>= decodeParseValidate
|
||||
tGet th@THandle {sessionId, thVersion = v} = liftIO (tGetParse th) >>= decodeParseValidate
|
||||
where
|
||||
decodeParseValidate :: Either TransportError RawTransmission -> m (SignedTransmission cmd)
|
||||
decodeParseValidate = \case
|
||||
@@ -768,5 +858,5 @@ tGet th@THandle {sessionId} = liftIO (tGetParse th) >>= decodeParseValidate
|
||||
|
||||
tParseValidate :: ByteString -> SignedRawTransmission -> m (SignedTransmission cmd)
|
||||
tParseValidate signed t@(sig, corrId, entityId, command) = do
|
||||
let cmd = parseProtocol command >>= checkCredentials t
|
||||
let cmd = parseProtocol v command >>= checkCredentials t
|
||||
pure (sig, signed, (CorrId corrId, entityId, cmd))
|
||||
|
||||
+189
-87
@@ -7,6 +7,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
@@ -41,17 +42,22 @@ import Control.Monad.Reader
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.List (intercalate)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Calendar.Month.Compat (pattern MonthDay)
|
||||
import Data.Time.Calendar.OrdinalDate (mondayStartWeek)
|
||||
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import Data.Type.Equality
|
||||
import Network.Socket (ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding (Encoding (smpEncode))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
@@ -159,9 +165,9 @@ smpServer started = do
|
||||
logServerStats :: Int -> Int -> m ()
|
||||
logServerStats startAt logInterval = do
|
||||
initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime
|
||||
logInfo $ "fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,msgQueues"
|
||||
logInfo $ "fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues"
|
||||
threadDelay $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0)
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeleted, msgSent, msgRecv, msgQueues} <- asks serverStats
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeleted, msgSent, msgRecv, dayMsgQueues, weekMsgQueues, monthMsgQueues} <- asks serverStats
|
||||
let interval = 1000000 * logInterval
|
||||
forever $ do
|
||||
ts <- liftIO getCurrentTime
|
||||
@@ -171,14 +177,23 @@ smpServer started = do
|
||||
qDeleted' <- atomically $ swapTVar qDeleted 0
|
||||
msgSent' <- atomically $ swapTVar msgSent 0
|
||||
msgRecv' <- atomically $ swapTVar msgRecv 0
|
||||
msgQueues' <- atomically $ S.size <$> swapTVar msgQueues S.empty
|
||||
logInfo . T.pack $ intercalate "," [show fromTime', show qCreated', show qSecured', show qDeleted', show msgSent', show msgRecv', show msgQueues']
|
||||
let day = utctDay ts
|
||||
(_, wDay) = mondayStartWeek day
|
||||
MonthDay _ mDay = day
|
||||
(dayMsgQueues', weekMsgQueues', monthMsgQueues') <-
|
||||
atomically $ (,,) <$> periodCount 1 dayMsgQueues <*> periodCount wDay weekMsgQueues <*> periodCount mDay monthMsgQueues
|
||||
logInfo . T.pack $ intercalate "," [show fromTime', show qCreated', show qSecured', show qDeleted', show msgSent', show msgRecv', show dayMsgQueues', weekMsgQueues', monthMsgQueues']
|
||||
threadDelay interval
|
||||
where
|
||||
periodCount :: Int -> TVar (Set RecipientId) -> STM String
|
||||
periodCount 1 pVar = show . S.size <$> swapTVar pVar S.empty
|
||||
periodCount _ _ = pure ""
|
||||
|
||||
runClient :: Transport c => TProxy c -> c -> m ()
|
||||
runClient _ h = do
|
||||
kh <- asks serverIdentity
|
||||
liftIO (runExceptT $ smpServerHandshake h kh) >>= \case
|
||||
smpVRange <- asks $ smpServerVRange . config
|
||||
liftIO (runExceptT $ smpServerHandshake h kh smpVRange) >>= \case
|
||||
Right th -> runClientTransport th
|
||||
Left _ -> pure ()
|
||||
|
||||
@@ -212,10 +227,11 @@ clientDisconnected c@Client {subscriptions, connected} = do
|
||||
sameClientSession :: Client -> Client -> Bool
|
||||
sameClientSession Client {sessionId} Client {sessionId = s'} = sessionId == s'
|
||||
|
||||
cancelSub :: MonadUnliftIO m => Sub -> m ()
|
||||
cancelSub = \case
|
||||
Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread
|
||||
_ -> return ()
|
||||
cancelSub :: MonadUnliftIO m => TVar Sub -> m ()
|
||||
cancelSub sub =
|
||||
readTVarIO sub >>= \case
|
||||
Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread
|
||||
_ -> return ()
|
||||
|
||||
receive :: (Transport c, MonadUnliftIO m, MonadReader Env m) => THandle c -> Client -> m ()
|
||||
receive th Client {rcvQ, sndQ, activeAt} = forever $ do
|
||||
@@ -232,10 +248,10 @@ receive th Client {rcvQ, sndQ, activeAt} = forever $ do
|
||||
write q t = atomically $ writeTBQueue q t
|
||||
|
||||
send :: (Transport c, MonadUnliftIO m) => THandle c -> Client -> m ()
|
||||
send h Client {sndQ, sessionId, activeAt} = forever $ do
|
||||
send h@THandle {thVersion = v} Client {sndQ, sessionId, activeAt} = forever $ do
|
||||
t <- atomically $ readTBQueue sndQ
|
||||
-- TODO the line below can return Left, but we ignore it and do not disconnect the client
|
||||
void . liftIO $ tPut h (Nothing, encodeTransmission sessionId t)
|
||||
void . liftIO $ tPut h (Nothing, encodeTransmission v sessionId t)
|
||||
atomically . writeTVar activeAt =<< liftIO getSystemTime
|
||||
|
||||
disconnectTransport :: (Transport c, MonadUnliftIO m) => THandle c -> client -> (client -> TVar SystemTime) -> ExpirationConfig -> m ()
|
||||
@@ -253,9 +269,9 @@ verifyTransmission sig_ signed queueId cmd = do
|
||||
case cmd of
|
||||
Cmd SRecipient (NEW k _) -> pure $ verifyCmdSignature sig_ signed k
|
||||
Cmd SRecipient _ -> verifyCmd SRecipient $ verifyCmdSignature sig_ signed . recipientKey
|
||||
Cmd SSender (SEND _) -> verifyCmd SSender $ verifyMaybe . senderKey
|
||||
Cmd SSender SEND {} -> verifyCmd SSender $ verifyMaybe . senderKey
|
||||
Cmd SSender PING -> pure True
|
||||
Cmd SNotifier NSUB -> verifyCmd SNotifier $ verifyMaybe . fmap snd . notifier
|
||||
Cmd SNotifier NSUB -> verifyCmd SNotifier $ verifyMaybe . fmap notifierKey . notifier
|
||||
where
|
||||
verifyCmd :: SParty p -> (QueueRec -> Bool) -> m Bool
|
||||
verifyCmd party f = do
|
||||
@@ -303,7 +319,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
case cmd of
|
||||
Cmd SSender command ->
|
||||
case command of
|
||||
SEND msgBody -> sendMessage st msgBody
|
||||
SEND flags msgBody -> sendMessage st flags msgBody
|
||||
PING -> pure (corrId, "", PONG)
|
||||
Cmd SNotifier NSUB -> subscribeNotifications
|
||||
Cmd SRecipient command ->
|
||||
@@ -314,9 +330,11 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
(createQueue st rKey dhKey)
|
||||
(pure (corrId, queueId, ERR AUTH))
|
||||
SUB -> subscribeQueue queueId
|
||||
ACK -> acknowledgeMsg
|
||||
GET -> getMessage
|
||||
ACK msgId -> acknowledgeMsg msgId
|
||||
KEY sKey -> secureQueue_ st sKey
|
||||
NKEY nKey -> addQueueNotifier_ st nKey
|
||||
NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey
|
||||
NDEL -> deleteQueueNotifier_ st
|
||||
OFF -> suspendQueue_ st
|
||||
DEL -> delQueueAndMsgs st
|
||||
where
|
||||
@@ -370,19 +388,28 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
atomically $ modifyTVar (qSecured stats) (+ 1)
|
||||
atomically $ (corrId,queueId,) . either ERR (const OK) <$> secureQueue st queueId sKey
|
||||
|
||||
addQueueNotifier_ :: QueueStore -> NtfPublicVerifyKey -> m (Transmission BrokerMsg)
|
||||
addQueueNotifier_ st nKey = (corrId,queueId,) <$> addNotifierRetry 3
|
||||
addQueueNotifier_ :: QueueStore -> NtfPublicVerifyKey -> RcvNtfPublicDhKey -> m (Transmission BrokerMsg)
|
||||
addQueueNotifier_ st notifierKey dhKey = do
|
||||
(rcvPublicDhKey, privDhKey) <- liftIO C.generateKeyPair'
|
||||
let rcvNtfDhSecret = C.dh' dhKey privDhKey
|
||||
(corrId,queueId,) <$> addNotifierRetry 3 rcvPublicDhKey rcvNtfDhSecret
|
||||
where
|
||||
addNotifierRetry :: Int -> m BrokerMsg
|
||||
addNotifierRetry 0 = pure $ ERR INTERNAL
|
||||
addNotifierRetry n = do
|
||||
nId <- randomId =<< asks (queueIdBytes . config)
|
||||
atomically (addQueueNotifier st queueId nId nKey) >>= \case
|
||||
Left DUPLICATE_ -> addNotifierRetry $ n - 1
|
||||
addNotifierRetry :: Int -> RcvNtfPublicDhKey -> RcvNtfDhSecret -> m BrokerMsg
|
||||
addNotifierRetry 0 _ _ = pure $ ERR INTERNAL
|
||||
addNotifierRetry n rcvPublicDhKey rcvNtfDhSecret = do
|
||||
notifierId <- randomId =<< asks (queueIdBytes . config)
|
||||
let ntfCreds = NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}
|
||||
atomically (addQueueNotifier st queueId ntfCreds) >>= \case
|
||||
Left DUPLICATE_ -> addNotifierRetry (n - 1) rcvPublicDhKey rcvNtfDhSecret
|
||||
Left e -> pure $ ERR e
|
||||
Right _ -> do
|
||||
withLog $ \s -> logAddNotifier s queueId nId nKey
|
||||
pure $ NID nId
|
||||
withLog $ \s -> logAddNotifier s queueId ntfCreds
|
||||
pure $ NID notifierId rcvPublicDhKey
|
||||
|
||||
deleteQueueNotifier_ :: QueueStore -> m (Transmission BrokerMsg)
|
||||
deleteQueueNotifier_ st = do
|
||||
withLog (`logDeleteNotifier` queueId)
|
||||
okResp <$> atomically (deleteQueueNotifier st queueId)
|
||||
|
||||
suspendQueue_ :: QueueStore -> m (Transmission BrokerMsg)
|
||||
suspendQueue_ st = do
|
||||
@@ -391,17 +418,55 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
|
||||
subscribeQueue :: RecipientId -> m (Transmission BrokerMsg)
|
||||
subscribeQueue rId =
|
||||
atomically (getSubscription rId) >>= deliverMessage tryPeekMsg rId
|
||||
|
||||
getSubscription :: RecipientId -> STM Sub
|
||||
getSubscription rId = do
|
||||
TM.lookup rId subscriptions >>= \case
|
||||
Just s -> tryTakeTMVar (delivered s) $> s
|
||||
Nothing -> do
|
||||
atomically (TM.lookup rId subscriptions) >>= \case
|
||||
Nothing ->
|
||||
atomically newSub >>= deliver
|
||||
Just sub ->
|
||||
readTVarIO sub >>= \case
|
||||
Sub {subThread = ProhibitSub} ->
|
||||
-- cannot use SUB in the same connection where GET was used
|
||||
pure (corrId, rId, ERR $ CMD PROHIBITED)
|
||||
s ->
|
||||
atomically (tryTakeTMVar $ delivered s) >> deliver sub
|
||||
where
|
||||
newSub :: STM (TVar Sub)
|
||||
newSub = do
|
||||
writeTBQueue subscribedQ (rId, clnt)
|
||||
s <- newSubscription
|
||||
TM.insert rId s subscriptions
|
||||
return s
|
||||
sub <- newTVar =<< newSubscription NoSub
|
||||
TM.insert rId sub subscriptions
|
||||
pure sub
|
||||
deliver :: TVar Sub -> m (Transmission BrokerMsg)
|
||||
deliver sub = do
|
||||
q <- getStoreMsgQueue rId
|
||||
msg_ <- atomically $ tryPeekMsg q
|
||||
deliverMessage rId sub q msg_
|
||||
|
||||
getMessage :: m (Transmission BrokerMsg)
|
||||
getMessage =
|
||||
atomically (TM.lookup queueId subscriptions) >>= \case
|
||||
Nothing ->
|
||||
atomically newSub >>= getMessage_
|
||||
Just sub ->
|
||||
readTVarIO sub >>= \case
|
||||
s@Sub {subThread = ProhibitSub} ->
|
||||
atomically (tryTakeTMVar $ delivered s)
|
||||
>> getMessage_ s
|
||||
-- cannot use GET in the same connection where there is an active subscription
|
||||
_ -> pure (corrId, queueId, ERR $ CMD PROHIBITED)
|
||||
where
|
||||
newSub :: STM Sub
|
||||
newSub = do
|
||||
s <- newSubscription ProhibitSub
|
||||
sub <- newTVar s
|
||||
TM.insert queueId sub subscriptions
|
||||
pure s
|
||||
getMessage_ :: Sub -> m (Transmission BrokerMsg)
|
||||
getMessage_ s = do
|
||||
q <- getStoreMsgQueue queueId
|
||||
atomically $
|
||||
tryPeekMsg q >>= \case
|
||||
Just msg -> setDelivered s msg $> (corrId, queueId, msgCmd msg)
|
||||
_ -> pure (corrId, queueId, OK)
|
||||
|
||||
subscribeNotifications :: m (Transmission BrokerMsg)
|
||||
subscribeNotifications = atomically $ do
|
||||
@@ -410,22 +475,48 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
TM.insert queueId () ntfSubscriptions
|
||||
pure ok
|
||||
|
||||
acknowledgeMsg :: m (Transmission BrokerMsg)
|
||||
acknowledgeMsg =
|
||||
atomically (withSub queueId $ \s -> const s <$$> tryTakeTMVar (delivered s))
|
||||
>>= \case
|
||||
Just (Just s) -> do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar (msgRecv stats) (+ 1)
|
||||
atomically $ modifyTVar (msgQueues stats) (S.insert queueId)
|
||||
deliverMessage tryDelPeekMsg queueId s
|
||||
_ -> return $ err NO_MSG
|
||||
acknowledgeMsg :: MsgId -> m (Transmission BrokerMsg)
|
||||
acknowledgeMsg msgId = do
|
||||
atomically (TM.lookup queueId subscriptions) >>= \case
|
||||
Nothing -> pure $ err NO_MSG
|
||||
Just sub ->
|
||||
atomically (getDelivered sub) >>= \case
|
||||
Just s -> do
|
||||
q <- getStoreMsgQueue queueId
|
||||
case s of
|
||||
Sub {subThread = ProhibitSub} -> do
|
||||
msgDeleted <- atomically $ tryDelMsg q msgId
|
||||
when msgDeleted updateStats
|
||||
pure ok
|
||||
_ -> do
|
||||
(msgDeleted, msg_) <- atomically $ tryDelPeekMsg q msgId
|
||||
when msgDeleted updateStats
|
||||
deliverMessage queueId sub q msg_
|
||||
_ -> pure $ err NO_MSG
|
||||
where
|
||||
getDelivered :: TVar Sub -> STM (Maybe Sub)
|
||||
getDelivered sub = do
|
||||
s@Sub {delivered} <- readTVar sub
|
||||
tryTakeTMVar delivered $>>= \msgId' ->
|
||||
if msgId == msgId' || B.null msgId
|
||||
then pure $ Just s
|
||||
else putTMVar delivered msgId' $> Nothing
|
||||
updateStats :: m ()
|
||||
updateStats = do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar (msgRecv stats) (+ 1)
|
||||
atomically $ updateActiveQueues stats queueId
|
||||
|
||||
withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a)
|
||||
withSub rId f = mapM f =<< TM.lookup rId subscriptions
|
||||
updateActiveQueues :: ServerStats -> RecipientId -> STM ()
|
||||
updateActiveQueues stats qId = do
|
||||
updatePeriod dayMsgQueues
|
||||
updatePeriod weekMsgQueues
|
||||
updatePeriod monthMsgQueues
|
||||
where
|
||||
updatePeriod pSel = modifyTVar (pSel stats) (S.insert qId)
|
||||
|
||||
sendMessage :: QueueStore -> MsgBody -> m (Transmission BrokerMsg)
|
||||
sendMessage st msgBody
|
||||
sendMessage :: QueueStore -> MsgFlags -> MsgBody -> m (Transmission BrokerMsg)
|
||||
sendMessage st flags msgBody
|
||||
| B.length msgBody > maxMessageLength = pure $ err LARGE_MSG
|
||||
| otherwise = do
|
||||
qr <- atomically $ getQueue st SSender queueId
|
||||
@@ -441,17 +532,18 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
ms <- asks msgStore
|
||||
ServerConfig {messageExpiration, msgQueueQuota} <- asks config
|
||||
old <- liftIO $ mapM expireBeforeEpoch messageExpiration
|
||||
ntfNonceDrg <- asks idsDrg
|
||||
resp@(_, _, sent) <- atomically $ do
|
||||
q <- getMsgQueue ms (recipientId qr) msgQueueQuota
|
||||
mapM_ (deleteExpiredMsgs q) old
|
||||
ifM (isFull q) (pure $ err QUOTA) $ do
|
||||
trySendNotification
|
||||
when (notification flags) $ trySendNotification msg ntfNonceDrg
|
||||
writeMsg q msg
|
||||
pure ok
|
||||
when (sent == OK) $ do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar (msgSent stats) (+ 1)
|
||||
atomically $ modifyTVar (msgQueues stats) (S.insert $ recipientId qr)
|
||||
atomically $ updateActiveQueues stats $ recipientId qr
|
||||
pure resp
|
||||
where
|
||||
mkMessage :: m (Either C.CryptoError Message)
|
||||
@@ -459,52 +551,62 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
|
||||
msgId <- randomId =<< asks (msgIdBytes . config)
|
||||
ts <- liftIO getSystemTime
|
||||
let c = C.cbEncrypt (rcvDhSecret qr) (C.cbNonce msgId) msgBody (maxMessageLength + 2)
|
||||
pure $ Message msgId ts <$> c
|
||||
pure $ Message msgId ts flags <$> c
|
||||
|
||||
trySendNotification :: STM ()
|
||||
trySendNotification =
|
||||
forM_ (notifier qr) $ \(nId, _) ->
|
||||
mapM_ (writeNtf nId) =<< TM.lookup nId notifiers
|
||||
trySendNotification :: Message -> TVar ChaChaDRG -> STM ()
|
||||
trySendNotification msg ntfNonceDrg =
|
||||
forM_ (notifier qr) $ \NtfCreds {notifierId, rcvNtfDhSecret} ->
|
||||
mapM_ (writeNtf notifierId msg rcvNtfDhSecret ntfNonceDrg) =<< TM.lookup notifierId notifiers
|
||||
|
||||
writeNtf :: NotifierId -> Client -> STM ()
|
||||
writeNtf nId Client {sndQ = q} =
|
||||
unlessM (isFullTBQueue sndQ) $
|
||||
writeTBQueue q (CorrId "", nId, NMSG)
|
||||
writeNtf :: NotifierId -> Message -> RcvNtfDhSecret -> TVar ChaChaDRG -> Client -> STM ()
|
||||
writeNtf nId msg rcvNtfDhSecret ntfNonceDrg Client {sndQ = q} =
|
||||
unlessM (isFullTBQueue sndQ) $ do
|
||||
(nmsgNonce, encNMsgMeta) <- mkMessageNotification msg rcvNtfDhSecret ntfNonceDrg
|
||||
writeTBQueue q (CorrId "", nId, NMSG nmsgNonce encNMsgMeta)
|
||||
|
||||
deliverMessage :: (MsgQueue -> STM (Maybe Message)) -> RecipientId -> Sub -> m (Transmission BrokerMsg)
|
||||
deliverMessage tryPeek rId = \case
|
||||
Sub {subThread = NoSub} -> do
|
||||
ms <- asks msgStore
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
q <- atomically $ getMsgQueue ms rId quota
|
||||
atomically (tryPeek q) >>= \case
|
||||
Nothing -> forkSub q $> ok
|
||||
Just msg -> atomically setDelivered $> (corrId, rId, msgCmd msg)
|
||||
_ -> pure ok
|
||||
mkMessageNotification :: Message -> RcvNtfDhSecret -> TVar ChaChaDRG -> STM (C.CbNonce, EncNMsgMeta)
|
||||
mkMessageNotification Message {msgId, ts} rcvNtfDhSecret ntfNonceDrg = do
|
||||
cbNonce <- C.pseudoRandomCbNonce ntfNonceDrg
|
||||
let msgMeta = NMsgMeta {msgId, msgTs = ts}
|
||||
encNMsgMeta = C.cbEncrypt rcvNtfDhSecret cbNonce (smpEncode msgMeta) 128
|
||||
pure . (cbNonce,) $ fromRight "" encNMsgMeta
|
||||
|
||||
deliverMessage :: RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> m (Transmission BrokerMsg)
|
||||
deliverMessage rId sub q msg_ =
|
||||
readTVarIO sub >>= \case
|
||||
s@Sub {subThread = NoSub} ->
|
||||
case msg_ of
|
||||
Just msg -> atomically (setDelivered s msg) $> (corrId, rId, msgCmd msg)
|
||||
_ -> forkSub $> ok
|
||||
_ -> pure ok
|
||||
where
|
||||
forkSub :: MsgQueue -> m ()
|
||||
forkSub q = do
|
||||
atomically . setSub $ \s -> s {subThread = SubPending}
|
||||
t <- mkWeakThreadId =<< forkIO (subscriber q)
|
||||
atomically . setSub $ \case
|
||||
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
|
||||
|
||||
subscriber :: MsgQueue -> m ()
|
||||
subscriber q = atomically $ do
|
||||
subscriber :: m ()
|
||||
subscriber = atomically $ do
|
||||
msg <- peekMsg q
|
||||
writeTBQueue sndQ (CorrId "", rId, msgCmd msg)
|
||||
setSub (\s -> s {subThread = NoSub})
|
||||
void setDelivered
|
||||
s <- readTVar sub
|
||||
void $ setDelivered s msg
|
||||
writeTVar sub s {subThread = NoSub}
|
||||
|
||||
setSub :: (Sub -> Sub) -> STM ()
|
||||
setSub f = TM.adjust f rId subscriptions
|
||||
setDelivered :: Sub -> Message -> STM Bool
|
||||
setDelivered s Message {msgId} = tryPutTMVar (delivered s) msgId
|
||||
|
||||
setDelivered :: STM (Maybe Bool)
|
||||
setDelivered = withSub rId $ \s -> tryPutTMVar (delivered s) ()
|
||||
getStoreMsgQueue :: RecipientId -> m MsgQueue
|
||||
getStoreMsgQueue rId = do
|
||||
ms <- asks msgStore
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
atomically $ getMsgQueue ms rId quota
|
||||
|
||||
msgCmd :: Message -> BrokerMsg
|
||||
msgCmd Message {msgId, ts, msgBody} = MSG msgId ts msgBody
|
||||
msgCmd :: Message -> BrokerMsg
|
||||
msgCmd Message {msgId, ts, msgFlags, msgBody} = MSG msgId ts msgFlags msgBody
|
||||
|
||||
delQueueAndMsgs :: QueueStore -> m (Transmission BrokerMsg)
|
||||
delQueueAndMsgs st = do
|
||||
@@ -565,4 +667,4 @@ restoreServerMessages = asks (storeMsgsFile . config) >>= mapM_ restoreMessages
|
||||
full <- atomically $ do
|
||||
q <- getMsgQueue ms rId quota
|
||||
ifM (isFull q) (pure True) (writeMsg q msg $> False)
|
||||
when full . B.putStrLn $ "message queue " <> strEncode rId <> " is full, message not restored: " <> strEncode (msgId msg)
|
||||
when full . B.putStrLn $ "message queue " <> strEncode rId <> " is full, message not restored: " <> strEncode (msgId (msg :: Message))
|
||||
|
||||
@@ -24,13 +24,14 @@ import Simplex.Messaging.Crypto (KeyHash (..))
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.MsgStore.STM
|
||||
import Simplex.Messaging.Server.QueueStore (QueueRec (..))
|
||||
import Simplex.Messaging.Server.QueueStore (NtfCreds (..), QueueRec (..))
|
||||
import Simplex.Messaging.Server.QueueStore.STM
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport)
|
||||
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Version
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
@@ -59,7 +60,9 @@ data ServerConfig = ServerConfig
|
||||
-- | CA certificate private key is not needed for initialization
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath
|
||||
certificateFile :: FilePath,
|
||||
-- | SMP client-server protocol version range
|
||||
smpServerVRange :: VersionRange
|
||||
}
|
||||
|
||||
defaultMessageExpiration :: ExpirationConfig
|
||||
@@ -96,7 +99,7 @@ data Server = Server
|
||||
}
|
||||
|
||||
data Client = Client
|
||||
{ subscriptions :: TMap RecipientId Sub,
|
||||
{ subscriptions :: TMap RecipientId (TVar Sub),
|
||||
ntfSubscriptions :: TMap NotifierId (),
|
||||
rcvQ :: TBQueue (Transmission Cmd),
|
||||
sndQ :: TBQueue (Transmission BrokerMsg),
|
||||
@@ -111,15 +114,17 @@ data ServerStats = ServerStats
|
||||
qDeleted :: TVar Int,
|
||||
msgSent :: TVar Int,
|
||||
msgRecv :: TVar Int,
|
||||
msgQueues :: TVar (Set RecipientId),
|
||||
dayMsgQueues :: TVar (Set RecipientId),
|
||||
weekMsgQueues :: TVar (Set RecipientId),
|
||||
monthMsgQueues :: TVar (Set RecipientId),
|
||||
fromTime :: TVar UTCTime
|
||||
}
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId)
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub
|
||||
|
||||
data Sub = Sub
|
||||
{ subThread :: SubscriptionThread,
|
||||
delivered :: TMVar ()
|
||||
delivered :: TMVar MsgId
|
||||
}
|
||||
|
||||
newServer :: Natural -> STM Server
|
||||
@@ -147,14 +152,16 @@ newServerStats ts = do
|
||||
qDeleted <- newTVar 0
|
||||
msgSent <- newTVar 0
|
||||
msgRecv <- newTVar 0
|
||||
msgQueues <- newTVar S.empty
|
||||
dayMsgQueues <- newTVar S.empty
|
||||
weekMsgQueues <- newTVar S.empty
|
||||
monthMsgQueues <- newTVar S.empty
|
||||
fromTime <- newTVar ts
|
||||
pure ServerStats {qCreated, qSecured, qDeleted, msgSent, msgRecv, msgQueues, fromTime}
|
||||
pure ServerStats {qCreated, qSecured, qDeleted, msgSent, msgRecv, dayMsgQueues, weekMsgQueues, monthMsgQueues, fromTime}
|
||||
|
||||
newSubscription :: STM Sub
|
||||
newSubscription = do
|
||||
newSubscription :: SubscriptionThread -> STM Sub
|
||||
newSubscription subThread = do
|
||||
delivered <- newEmptyTMVar
|
||||
return Sub {subThread = NoSub, delivered}
|
||||
return Sub {subThread, delivered}
|
||||
|
||||
newEnv :: forall m. (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
|
||||
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do
|
||||
@@ -183,4 +190,4 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile,
|
||||
addNotifier :: QueueRec -> Map NotifierId RecipientId -> Map NotifierId RecipientId
|
||||
addNotifier q = case notifier q of
|
||||
Nothing -> id
|
||||
Just (nId, _) -> M.insert nId (recipientId q)
|
||||
Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q)
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Server.MsgStore where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (MsgBody, MsgId, RecipientId)
|
||||
import Simplex.Messaging.Protocol (MsgBody, MsgFlags, MsgId, RecipientId, noMsgFlags)
|
||||
|
||||
data Message = Message
|
||||
{ msgId :: MsgId,
|
||||
ts :: SystemTime,
|
||||
msgFlags :: MsgFlags,
|
||||
msgBody :: MsgBody
|
||||
}
|
||||
|
||||
instance StrEncoding Message where
|
||||
strEncode Message {msgId, ts, msgBody} = strEncode (msgId, ts, msgBody)
|
||||
strEncode Message {msgId, ts, msgFlags, msgBody} =
|
||||
B.unwords
|
||||
[ strEncode msgId,
|
||||
strEncode ts,
|
||||
"flags=" <> strEncode msgFlags,
|
||||
strEncode msgBody
|
||||
]
|
||||
strP = do
|
||||
(msgId, ts, msgBody) <- strP
|
||||
pure Message {msgId, ts, msgBody}
|
||||
msgId <- strP_
|
||||
ts <- strP_
|
||||
msgFlags <- ("flags=" *> strP_) <|> pure noMsgFlags
|
||||
msgBody <- strP
|
||||
pure Message {msgId, ts, msgFlags, msgBody}
|
||||
|
||||
data MsgLogRecord = MsgLogRecord RecipientId Message
|
||||
|
||||
@@ -37,5 +50,6 @@ class MonadMsgQueue q m where
|
||||
writeMsg :: q -> Message -> m () -- non blocking
|
||||
tryPeekMsg :: q -> m (Maybe Message) -- non blocking
|
||||
peekMsg :: q -> m Message -- blocking
|
||||
tryDelPeekMsg :: q -> m (Maybe Message) -- atomic delete (== read) last and peek next message, if available
|
||||
tryDelMsg :: q -> MsgId -> m Bool -- non blocking
|
||||
tryDelPeekMsg :: q -> MsgId -> m (Bool, Maybe Message) -- atomic delete (== read) last and peek next message, if available
|
||||
deleteExpiredMsgs :: q -> Int64 -> m ()
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Server.MsgStore.STM where
|
||||
|
||||
import Control.Concurrent.STM.TBQueue (flushTBQueue)
|
||||
import Control.Monad (when)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.Time.Clock.System (SystemTime (systemSeconds))
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Protocol (RecipientId)
|
||||
import Simplex.Messaging.Protocol (MsgId, RecipientId)
|
||||
import Simplex.Messaging.Server.MsgStore
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -53,9 +57,22 @@ instance MonadMsgQueue MsgQueue STM where
|
||||
peekMsg :: MsgQueue -> STM Message
|
||||
peekMsg = peekTBQueue . msgQueue
|
||||
|
||||
tryDelMsg :: MsgQueue -> MsgId -> STM Bool
|
||||
tryDelMsg (MsgQueue q) msgId' =
|
||||
tryPeekTBQueue q >>= \case
|
||||
Just Message {msgId}
|
||||
| msgId == msgId' || B.null msgId' -> tryReadTBQueue q $> True
|
||||
| otherwise -> pure False
|
||||
_ -> pure False
|
||||
|
||||
-- atomic delete (== read) last and peek next message if available
|
||||
tryDelPeekMsg :: MsgQueue -> STM (Maybe Message)
|
||||
tryDelPeekMsg (MsgQueue q) = tryReadTBQueue q >> tryPeekTBQueue q
|
||||
tryDelPeekMsg :: MsgQueue -> MsgId -> STM (Bool, Maybe Message)
|
||||
tryDelPeekMsg (MsgQueue q) msgId' =
|
||||
tryPeekTBQueue q >>= \case
|
||||
msg_@(Just Message {msgId})
|
||||
| msgId == msgId' || B.null msgId' -> (True,) <$> (tryReadTBQueue q >> tryPeekTBQueue q)
|
||||
| otherwise -> pure (False, msg_)
|
||||
_ -> pure (False, Nothing)
|
||||
|
||||
deleteExpiredMsgs :: MsgQueue -> Int64 -> STM ()
|
||||
deleteExpiredMsgs (MsgQueue q) old = loop
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Messaging.Server.QueueStore where
|
||||
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
|
||||
data QueueRec = QueueRec
|
||||
@@ -12,17 +14,31 @@ data QueueRec = QueueRec
|
||||
rcvDhSecret :: RcvDhSecret,
|
||||
senderId :: SenderId,
|
||||
senderKey :: Maybe SndPublicVerifyKey,
|
||||
notifier :: Maybe (NotifierId, NtfPublicVerifyKey),
|
||||
notifier :: Maybe NtfCreds,
|
||||
status :: QueueStatus
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data NtfCreds = NtfCreds
|
||||
{ notifierId :: NotifierId,
|
||||
notifierKey :: NtfPublicVerifyKey,
|
||||
rcvNtfDhSecret :: RcvNtfDhSecret
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding NtfCreds where
|
||||
strEncode NtfCreds {notifierId, notifierKey, rcvNtfDhSecret} = strEncode (notifierId, notifierKey, rcvNtfDhSecret)
|
||||
strP = do
|
||||
(notifierId, notifierKey, rcvNtfDhSecret) <- strP
|
||||
pure NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}
|
||||
|
||||
data QueueStatus = QueueActive | QueueOff deriving (Eq, Show)
|
||||
|
||||
class MonadQueueStore s m where
|
||||
addQueue :: s -> QueueRec -> m (Either ErrorType ())
|
||||
getQueue :: s -> SParty p -> QueueId -> m (Either ErrorType QueueRec)
|
||||
secureQueue :: s -> RecipientId -> SndPublicVerifyKey -> m (Either ErrorType QueueRec)
|
||||
addQueueNotifier :: s -> RecipientId -> NotifierId -> NtfPublicVerifyKey -> m (Either ErrorType QueueRec)
|
||||
addQueueNotifier :: s -> RecipientId -> NtfCreds -> m (Either ErrorType QueueRec)
|
||||
deleteQueueNotifier :: s -> RecipientId -> m (Either ErrorType ())
|
||||
suspendQueue :: s -> RecipientId -> m (Either ErrorType ())
|
||||
deleteQueue :: s -> RecipientId -> m (Either ErrorType ())
|
||||
|
||||
@@ -61,16 +61,23 @@ instance MonadQueueStore QueueStore STM where
|
||||
Just _ -> pure Nothing
|
||||
_ -> writeTVar qVar q {senderKey = Just sKey} $> Just q
|
||||
|
||||
addQueueNotifier :: QueueStore -> RecipientId -> NotifierId -> NtfPublicVerifyKey -> STM (Either ErrorType QueueRec)
|
||||
addQueueNotifier QueueStore {queues, notifiers} rId nId nKey = do
|
||||
addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> STM (Either ErrorType QueueRec)
|
||||
addQueueNotifier QueueStore {queues, notifiers} rId ntfCreds@NtfCreds {notifierId = nId} = do
|
||||
ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $
|
||||
withQueue rId queues $ \qVar ->
|
||||
readTVar qVar >>= \q -> case notifier q of
|
||||
Just _ -> pure Nothing
|
||||
_ -> do
|
||||
writeTVar qVar q {notifier = Just (nId, nKey)}
|
||||
TM.insert nId rId notifiers
|
||||
pure $ Just q
|
||||
withQueue rId queues $ \qVar -> do
|
||||
q <- readTVar qVar
|
||||
forM_ (notifier q) $ (`TM.delete` notifiers) . notifierId
|
||||
writeTVar qVar q {notifier = Just ntfCreds}
|
||||
TM.insert nId rId notifiers
|
||||
pure $ Just q
|
||||
|
||||
deleteQueueNotifier :: QueueStore -> RecipientId -> STM (Either ErrorType ())
|
||||
deleteQueueNotifier QueueStore {queues, notifiers} rId =
|
||||
withQueue rId queues $ \qVar -> do
|
||||
q <- readTVar qVar
|
||||
forM_ (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers
|
||||
writeTVar qVar q {notifier = Nothing}
|
||||
pure $ Just ()
|
||||
|
||||
suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
|
||||
suspendQueue QueueStore {queues} rId =
|
||||
@@ -82,7 +89,7 @@ instance MonadQueueStore QueueStore STM where
|
||||
Just qVar ->
|
||||
readTVar qVar >>= \q -> do
|
||||
TM.delete (senderId q) senders
|
||||
forM_ (notifier q) $ \(nId, _) -> TM.delete nId notifiers
|
||||
forM_ (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers
|
||||
pure $ Right ()
|
||||
_ -> pure $ Left AUTH
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ module Simplex.Messaging.Server.StoreLog
|
||||
openReadStoreLog,
|
||||
storeLogFilePath,
|
||||
closeStoreLog,
|
||||
writeStoreLogRecord,
|
||||
logCreateQueue,
|
||||
logSecureQueue,
|
||||
logAddNotifier,
|
||||
logDeleteQueue,
|
||||
logDeleteNotifier,
|
||||
readWriteStoreLog,
|
||||
)
|
||||
where
|
||||
@@ -34,7 +36,7 @@ import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.QueueStore (QueueRec (..), QueueStatus (..))
|
||||
import Simplex.Messaging.Server.QueueStore (NtfCreds (..), QueueRec (..), QueueStatus (..))
|
||||
import Simplex.Messaging.Transport (trimCR)
|
||||
import System.Directory (doesFileExist)
|
||||
import System.IO
|
||||
@@ -48,8 +50,9 @@ data StoreLog (a :: IOMode) where
|
||||
data StoreLogRecord
|
||||
= CreateQueue QueueRec
|
||||
| SecureQueue QueueId SndPublicVerifyKey
|
||||
| AddNotifier QueueId NotifierId NtfPublicVerifyKey
|
||||
| AddNotifier QueueId NtfCreds
|
||||
| DeleteQueue QueueId
|
||||
| DeleteNotifier QueueId
|
||||
|
||||
instance StrEncoding QueueRec where
|
||||
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier} =
|
||||
@@ -62,7 +65,7 @@ instance StrEncoding QueueRec where
|
||||
]
|
||||
<> maybe "" notifierStr notifier
|
||||
where
|
||||
notifierStr (nId, nKey) = " nid=" <> strEncode nId <> " nk=" <> strEncode nKey
|
||||
notifierStr ntfCreds = " notifier=" <> strEncode ntfCreds
|
||||
|
||||
strP = do
|
||||
recipientId <- "rid=" *> strP_
|
||||
@@ -70,24 +73,29 @@ instance StrEncoding QueueRec where
|
||||
rcvDhSecret <- "rdh=" *> strP_
|
||||
senderId <- "sid=" *> strP_
|
||||
senderKey <- "sk=" *> strP
|
||||
notifier <- optional $ (,) <$> (" nid=" *> strP_) <*> ("nk=" *> strP)
|
||||
notifier <- optional $ " notifier=" *> strP
|
||||
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier, status = QueueActive}
|
||||
|
||||
instance StrEncoding StoreLogRecord where
|
||||
strEncode = \case
|
||||
CreateQueue q -> strEncode (Str "CREATE", q)
|
||||
SecureQueue rId sKey -> strEncode (Str "SECURE", rId, sKey)
|
||||
AddNotifier rId nId nKey -> strEncode (Str "NOTIFIER", rId, nId, nKey)
|
||||
AddNotifier rId ntfCreds -> strEncode (Str "NOTIFIER", rId, ntfCreds)
|
||||
DeleteQueue rId -> strEncode (Str "DELETE", rId)
|
||||
DeleteNotifier rId -> strEncode (Str "NDELETE", rId)
|
||||
|
||||
strP =
|
||||
"CREATE " *> (CreateQueue <$> strP)
|
||||
<|> "SECURE " *> (SecureQueue <$> strP_ <*> strP)
|
||||
<|> "NOTIFIER " *> (AddNotifier <$> strP_ <*> strP_ <*> strP)
|
||||
<|> "NOTIFIER " *> (AddNotifier <$> strP_ <*> strP)
|
||||
<|> "DELETE " *> (DeleteQueue <$> strP)
|
||||
<|> "NDELETE" *> (DeleteNotifier <$> strP)
|
||||
|
||||
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
|
||||
openWriteStoreLog f = WriteStoreLog f <$> openFile f WriteMode
|
||||
openWriteStoreLog f = do
|
||||
h <- openFile f WriteMode
|
||||
hSetBuffering h LineBuffering
|
||||
pure $ WriteStoreLog f h
|
||||
|
||||
openReadStoreLog :: FilePath -> IO (StoreLog 'ReadMode)
|
||||
openReadStoreLog f = do
|
||||
@@ -104,7 +112,7 @@ closeStoreLog = \case
|
||||
WriteStoreLog _ h -> hClose h
|
||||
ReadStoreLog _ h -> hClose h
|
||||
|
||||
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
|
||||
writeStoreLogRecord :: StrEncoding r => StoreLog 'WriteMode -> r -> IO ()
|
||||
writeStoreLogRecord (WriteStoreLog _ h) r = do
|
||||
B.hPutStrLn h $ strEncode r
|
||||
hFlush h
|
||||
@@ -115,12 +123,15 @@ logCreateQueue s = writeStoreLogRecord s . CreateQueue
|
||||
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SndPublicVerifyKey -> IO ()
|
||||
logSecureQueue s qId sKey = writeStoreLogRecord s $ SecureQueue qId sKey
|
||||
|
||||
logAddNotifier :: StoreLog 'WriteMode -> QueueId -> NotifierId -> NtfPublicVerifyKey -> IO ()
|
||||
logAddNotifier s qId nId nKey = writeStoreLogRecord s $ AddNotifier qId nId nKey
|
||||
logAddNotifier :: StoreLog 'WriteMode -> QueueId -> NtfCreds -> IO ()
|
||||
logAddNotifier s qId ntfCreds = writeStoreLogRecord s $ AddNotifier qId ntfCreds
|
||||
|
||||
logDeleteQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
|
||||
logDeleteQueue s = writeStoreLogRecord s . DeleteQueue
|
||||
|
||||
logDeleteNotifier :: StoreLog 'WriteMode -> QueueId -> IO ()
|
||||
logDeleteNotifier s = writeStoreLogRecord s . DeleteNotifier
|
||||
|
||||
readWriteStoreLog :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec, StoreLog 'WriteMode)
|
||||
readWriteStoreLog s@(ReadStoreLog f _) = do
|
||||
qs <- readQueues s
|
||||
@@ -149,7 +160,8 @@ readQueues (ReadStoreLog _ h) = LB.hGetContents h >>= returnResult . procStoreLo
|
||||
procLogRecord m = \case
|
||||
CreateQueue q -> M.insert (recipientId q) q m
|
||||
SecureQueue qId sKey -> M.adjust (\q -> q {senderKey = Just sKey}) qId m
|
||||
AddNotifier qId nId nKey -> M.adjust (\q -> q {notifier = Just (nId, nKey)}) qId m
|
||||
AddNotifier qId ntfCreds -> M.adjust (\q -> q {notifier = Just ntfCreds}) qId m
|
||||
DeleteQueue qId -> M.delete qId m
|
||||
DeleteNotifier qId -> M.adjust (\q -> q {notifier = Nothing}) qId m
|
||||
printError :: LogParsingError -> IO ()
|
||||
printError (e, s) = B.putStrLn $ "Error parsing log: " <> B.pack e <> " - " <> s
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
|
||||
module Simplex.Messaging.Transport
|
||||
( -- * SMP transport parameters
|
||||
supportedSMPVersions,
|
||||
supportedSMPServerVRange,
|
||||
simplexMQVersion,
|
||||
|
||||
-- * Transport connection class
|
||||
@@ -92,11 +92,11 @@ import UnliftIO.STM
|
||||
smpBlockSize :: Int
|
||||
smpBlockSize = 16384
|
||||
|
||||
supportedSMPVersions :: VersionRange
|
||||
supportedSMPVersions = mkVersionRange 1 1
|
||||
supportedSMPServerVRange :: VersionRange
|
||||
supportedSMPServerVRange = mkVersionRange 1 2
|
||||
|
||||
simplexMQVersion :: String
|
||||
simplexMQVersion = "2.3.0"
|
||||
simplexMQVersion = "3.0.0-beta.2"
|
||||
|
||||
-- * Transport connection class
|
||||
|
||||
@@ -214,10 +214,11 @@ instance Transport TLS where
|
||||
readChunks :: ByteString -> IO ByteString
|
||||
readChunks b
|
||||
| B.length b >= n = pure b
|
||||
| otherwise = readChunks . (b <>) =<< T.recvData tlsContext `E.catch` handleEOF
|
||||
handleEOF = \case
|
||||
T.Error_EOF -> E.throwIO TEBadBlock
|
||||
e -> E.throwIO e
|
||||
| otherwise =
|
||||
T.recvData tlsContext >>= \case
|
||||
-- https://hackage.haskell.org/package/tls-1.6.0/docs/Network-TLS.html#v:recvData
|
||||
"" -> ioe_EOF
|
||||
s -> readChunks $ b <> s
|
||||
|
||||
cPut :: TLS -> ByteString -> IO ()
|
||||
cPut tls = T.sendData (tlsContext tls) . BL.fromStrict
|
||||
@@ -351,28 +352,28 @@ tGetBlock THandle {connection = c, blockSize} =
|
||||
-- | 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 -> C.KeyHash -> ExceptT TransportError IO (THandle c)
|
||||
smpServerHandshake c kh = do
|
||||
smpServerHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
|
||||
smpServerHandshake c kh smpVRange = do
|
||||
let th@THandle {sessionId} = smpTHandle c
|
||||
sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = supportedSMPVersions}
|
||||
sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = smpVRange}
|
||||
getHandshake th >>= \case
|
||||
ClientHandshake {smpVersion, keyHash}
|
||||
| keyHash /= kh ->
|
||||
throwE $ TEHandshake IDENTITY
|
||||
| smpVersion `isCompatible` supportedSMPVersions -> do
|
||||
| smpVersion `isCompatible` smpVRange -> do
|
||||
pure (th :: THandle c) {thVersion = smpVersion}
|
||||
| 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.KeyHash -> ExceptT TransportError IO (THandle c)
|
||||
smpClientHandshake c keyHash = do
|
||||
smpClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
|
||||
smpClientHandshake c keyHash smpVRange = do
|
||||
let th@THandle {sessionId} = smpTHandle c
|
||||
ServerHandshake {sessionId = sessId, smpVersionRange} <- getHandshake th
|
||||
if sessionId /= sessId
|
||||
then throwE TEBadSession
|
||||
else case smpVersionRange `compatibleVersion` supportedSMPVersions of
|
||||
else case smpVersionRange `compatibleVersion` smpVRange of
|
||||
Just (Compatible smpVersion) -> do
|
||||
sendHandshake th $ ClientHandshake {smpVersion, keyHash}
|
||||
pure (th :: THandle c) {thVersion = smpVersion}
|
||||
|
||||
@@ -84,7 +84,7 @@ validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_,
|
||||
x509validate = XV.validate X.HashSHA256 hooks checks certStore cache serviceID cc
|
||||
where
|
||||
hooks = XV.defaultHooks
|
||||
checks = XV.defaultChecks
|
||||
checks = XV.defaultChecks {XV.checkFQHN = False}
|
||||
certStore = XS.makeCertificateStore sc
|
||||
cache = XV.exceptionValidationCache [] -- we manually check fingerprint only of the identity certificate (ca.crt)
|
||||
serviceID = (host, port)
|
||||
|
||||
@@ -54,8 +54,9 @@ runTCPServer started port server = do
|
||||
(closeServer started clients)
|
||||
$ \sock -> forever . E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do
|
||||
-- catchAll_ is needed here in case the connection was closed earlier
|
||||
cId <- atomically $ stateTVar clientId $ \cId -> (cId + 1, cId + 1)
|
||||
tId <- mkWeakThreadId =<< forkFinally (server conn) (const $ gracefulClose conn 5000 `catchAll_` atomically (TM.delete cId clients))
|
||||
cId <- atomically $ stateTVar clientId $ \cId -> let cId' = cId + 1 in (cId', cId')
|
||||
let closeConn _ = atomically (TM.delete cId clients) >> gracefulClose conn 5000 `catchAll_` pure ()
|
||||
tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn
|
||||
atomically $ TM.insert cId tId clients
|
||||
|
||||
closeServer :: TMVar Bool -> TMap Int (Weak ThreadId) -> Socket -> IO ()
|
||||
|
||||
@@ -10,6 +10,8 @@ import Control.Monad.Trans.Except
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import UnliftIO.Async
|
||||
|
||||
raceAny_ :: MonadUnliftIO m => [m a] -> m ()
|
||||
@@ -32,6 +34,10 @@ bshow :: Show a => a -> ByteString
|
||||
bshow = B.pack . show
|
||||
{-# INLINE bshow #-}
|
||||
|
||||
tshow :: Show a => a -> Text
|
||||
tshow = T.pack . show
|
||||
{-# INLINE tshow #-}
|
||||
|
||||
maybeWord :: (a -> ByteString) -> Maybe a -> ByteString
|
||||
maybeWord f = maybe "" $ B.cons ' ' . f
|
||||
{-# INLINE maybeWord #-}
|
||||
@@ -56,6 +62,10 @@ tryE :: Monad m => ExceptT e m a -> ExceptT e m (Either e a)
|
||||
tryE m = (Right <$> m) `catchE` (pure . Left)
|
||||
{-# INLINE tryE #-}
|
||||
|
||||
liftE :: (e -> e') -> ExceptT e IO a -> ExceptT e' IO a
|
||||
liftE f a = ExceptT $ first f <$> runExceptT a
|
||||
{-# INLINE liftE #-}
|
||||
|
||||
ifM :: Monad m => m Bool -> m a -> m a -> m a
|
||||
ifM ba t f = ba >>= \b -> if b then t else f
|
||||
{-# INLINE ifM #-}
|
||||
@@ -78,3 +88,7 @@ catchAll = E.catch
|
||||
catchAll_ :: IO a -> IO a -> IO a
|
||||
catchAll_ a = catchAll a . const
|
||||
{-# INLINE catchAll_ #-}
|
||||
|
||||
eitherToMaybe :: Either a b -> Maybe b
|
||||
eitherToMaybe = either (const Nothing) Just
|
||||
{-# INLINE eitherToMaybe #-}
|
||||
|
||||
+2
-3
@@ -17,7 +17,7 @@
|
||||
#
|
||||
# resolver: ./custom-snapshot.yaml
|
||||
# resolver: https://example.com/snapshots/2018-01-01.yaml
|
||||
resolver: lts-18.21
|
||||
resolver: lts-18.28
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
@@ -39,7 +39,7 @@ extra-deps:
|
||||
- network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888
|
||||
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
|
||||
- tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991
|
||||
# below dependancies are to update Aeson to 2.0.3
|
||||
# below dependancies are to update Aeson to 2.0.3
|
||||
- OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262
|
||||
- attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810
|
||||
- hashable-1.4.0.2@sha256:0cddd0229d1aac305ea0404409c0bbfab81f075817bd74b8b2929eff58333e55,5005
|
||||
@@ -53,7 +53,6 @@ extra-deps:
|
||||
# commit: f6cc753611f80af300401cfae63846e9d7c40d9e
|
||||
# subdirs:
|
||||
# - core
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
# flags: {}
|
||||
|
||||
|
||||
+76
-74
@@ -12,6 +12,7 @@ module AgentTests (agentTests) where
|
||||
import AgentTests.ConnectionRequestTests
|
||||
import AgentTests.DoubleRatchetTests (doubleRatchetTests)
|
||||
import AgentTests.FunctionalAPITests (functionalAPITests)
|
||||
import AgentTests.NotificationTests (notificationTests)
|
||||
import AgentTests.SQLiteTests (storeTests)
|
||||
import AgentTests.SchemaDump (schemaDumpTest)
|
||||
import Control.Concurrent
|
||||
@@ -36,6 +37,7 @@ agentTests (ATransport t) = do
|
||||
describe "Connection request" connectionRequestTests
|
||||
describe "Double ratchet tests" doubleRatchetTests
|
||||
describe "Functional API" $ functionalAPITests (ATransport t)
|
||||
describe "Notification tests" $ notificationTests (ATransport t)
|
||||
describe "SQLite store" storeTests
|
||||
describe "SQLite schema dump" schemaDumpTest
|
||||
describe "SMP agent protocol syntax" $ syntaxTests t
|
||||
@@ -117,7 +119,7 @@ h #:# err = tryGet `shouldReturn` ()
|
||||
_ -> return ()
|
||||
|
||||
pattern Msg :: MsgBody -> ACommand 'Agent
|
||||
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
|
||||
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} _ msgBody
|
||||
|
||||
testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testDuplexConnection _ alice bob = do
|
||||
@@ -130,25 +132,25 @@ testDuplexConnection _ alice bob = do
|
||||
bob <# ("", "alice", CON)
|
||||
alice <# ("", "bob", CON)
|
||||
-- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4
|
||||
alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", MID 5)
|
||||
alice <# ("", "bob", SENT 5)
|
||||
alice #: ("3", "bob", "SEND F :hello") #> ("3", "bob", MID 4)
|
||||
alice <# ("", "bob", SENT 4)
|
||||
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
|
||||
bob #: ("12", "alice", "ACK 5") #> ("12", "alice", OK)
|
||||
alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 6)
|
||||
alice <# ("", "bob", SENT 6)
|
||||
bob #: ("12", "alice", "ACK 4") #> ("12", "alice", OK)
|
||||
alice #: ("4", "bob", "SEND F :how are you?") #> ("4", "bob", MID 5)
|
||||
alice <# ("", "bob", SENT 5)
|
||||
bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False
|
||||
bob #: ("13", "alice", "ACK 6") #> ("13", "alice", OK)
|
||||
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 7)
|
||||
bob <# ("", "alice", SENT 7)
|
||||
bob #: ("13", "alice", "ACK 5") #> ("13", "alice", OK)
|
||||
bob #: ("14", "alice", "SEND F 9\nhello too") #> ("14", "alice", MID 6)
|
||||
bob <# ("", "alice", SENT 6)
|
||||
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
|
||||
alice #: ("3a", "bob", "ACK 7") #> ("3a", "bob", OK)
|
||||
bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 8)
|
||||
bob <# ("", "alice", SENT 8)
|
||||
alice #: ("3a", "bob", "ACK 6") #> ("3a", "bob", OK)
|
||||
bob #: ("15", "alice", "SEND F 9\nmessage 1") #> ("15", "alice", MID 7)
|
||||
bob <# ("", "alice", SENT 7)
|
||||
alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False
|
||||
alice #: ("4a", "bob", "ACK 8") #> ("4a", "bob", OK)
|
||||
alice #: ("4a", "bob", "ACK 7") #> ("4a", "bob", OK)
|
||||
alice #: ("5", "bob", "OFF") #> ("5", "bob", OK)
|
||||
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 9)
|
||||
bob <# ("", "alice", MERR 9 (SMP AUTH))
|
||||
bob #: ("17", "alice", "SEND F 9\nmessage 3") #> ("17", "alice", MID 8)
|
||||
bob <# ("", "alice", MERR 8 (SMP AUTH))
|
||||
alice #: ("6", "bob", "DEL") #> ("6", "bob", OK)
|
||||
alice #:# "nothing else should be delivered to alice"
|
||||
|
||||
@@ -163,25 +165,25 @@ testDuplexConnRandomIds _ alice bob = do
|
||||
bob <# ("", aliceConn, INFO "alice's connInfo")
|
||||
bob <# ("", aliceConn, CON)
|
||||
alice <# ("", bobConn, CON)
|
||||
alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 5)
|
||||
alice <# ("", bobConn, SENT 5)
|
||||
alice #: ("2", bobConn, "SEND F :hello") #> ("2", bobConn, MID 4)
|
||||
alice <# ("", bobConn, SENT 4)
|
||||
bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False
|
||||
bob #: ("12", aliceConn, "ACK 5") #> ("12", aliceConn, OK)
|
||||
alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 6)
|
||||
alice <# ("", bobConn, SENT 6)
|
||||
bob #: ("12", aliceConn, "ACK 4") #> ("12", aliceConn, OK)
|
||||
alice #: ("3", bobConn, "SEND F :how are you?") #> ("3", bobConn, MID 5)
|
||||
alice <# ("", bobConn, SENT 5)
|
||||
bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False
|
||||
bob #: ("13", aliceConn, "ACK 6") #> ("13", aliceConn, OK)
|
||||
bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 7)
|
||||
bob <# ("", aliceConn, SENT 7)
|
||||
bob #: ("13", aliceConn, "ACK 5") #> ("13", aliceConn, OK)
|
||||
bob #: ("14", aliceConn, "SEND F 9\nhello too") #> ("14", aliceConn, MID 6)
|
||||
bob <# ("", aliceConn, SENT 6)
|
||||
alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False
|
||||
alice #: ("3a", bobConn, "ACK 7") #> ("3a", bobConn, OK)
|
||||
bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 8)
|
||||
bob <# ("", aliceConn, SENT 8)
|
||||
alice #: ("3a", bobConn, "ACK 6") #> ("3a", bobConn, OK)
|
||||
bob #: ("15", aliceConn, "SEND F 9\nmessage 1") #> ("15", aliceConn, MID 7)
|
||||
bob <# ("", aliceConn, SENT 7)
|
||||
alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False
|
||||
alice #: ("4a", bobConn, "ACK 8") #> ("4a", bobConn, OK)
|
||||
alice #: ("4a", bobConn, "ACK 7") #> ("4a", bobConn, OK)
|
||||
alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK)
|
||||
bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 9)
|
||||
bob <# ("", aliceConn, MERR 9 (SMP AUTH))
|
||||
bob #: ("17", aliceConn, "SEND F 9\nmessage 3") #> ("17", aliceConn, MID 8)
|
||||
bob <# ("", aliceConn, MERR 8 (SMP AUTH))
|
||||
alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK)
|
||||
alice #:# "nothing else should be delivered to alice"
|
||||
|
||||
@@ -198,10 +200,10 @@ testContactConnection _ alice bob tom = do
|
||||
alice <# ("", "bob", INFO "bob's connInfo 2")
|
||||
alice <# ("", "bob", CON)
|
||||
bob <# ("", "alice", CON)
|
||||
alice #: ("3", "bob", "SEND :hi") #> ("3", "bob", MID 5)
|
||||
alice <# ("", "bob", SENT 5)
|
||||
alice #: ("3", "bob", "SEND F :hi") #> ("3", "bob", MID 4)
|
||||
alice <# ("", "bob", SENT 4)
|
||||
bob <#= \case ("", "alice", Msg "hi") -> True; _ -> False
|
||||
bob #: ("13", "alice", "ACK 5") #> ("13", "alice", OK)
|
||||
bob #: ("13", "alice", "ACK 4") #> ("13", "alice", OK)
|
||||
|
||||
tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK)
|
||||
("", "alice_contact", Right (REQ aInvId' "tom's connInfo")) <- (alice <#:)
|
||||
@@ -211,10 +213,10 @@ testContactConnection _ alice bob tom = do
|
||||
alice <# ("", "tom", INFO "tom's connInfo 2")
|
||||
alice <# ("", "tom", CON)
|
||||
tom <# ("", "alice", CON)
|
||||
alice #: ("5", "tom", "SEND :hi there") #> ("5", "tom", MID 5)
|
||||
alice <# ("", "tom", SENT 5)
|
||||
alice #: ("5", "tom", "SEND F :hi there") #> ("5", "tom", MID 4)
|
||||
alice <# ("", "tom", SENT 4)
|
||||
tom <#= \case ("", "alice", Msg "hi there") -> True; _ -> False
|
||||
tom #: ("23", "alice", "ACK 5") #> ("23", "alice", OK)
|
||||
tom #: ("23", "alice", "ACK 4") #> ("23", "alice", OK)
|
||||
|
||||
testContactConnRandomIds :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testContactConnRandomIds _ alice bob = do
|
||||
@@ -234,10 +236,10 @@ testContactConnRandomIds _ alice bob = do
|
||||
alice <# ("", bobConn, CON)
|
||||
bob <# ("", aliceConn, CON)
|
||||
|
||||
alice #: ("3", bobConn, "SEND :hi") #> ("3", bobConn, MID 5)
|
||||
alice <# ("", bobConn, SENT 5)
|
||||
alice #: ("3", bobConn, "SEND F :hi") #> ("3", bobConn, MID 4)
|
||||
alice <# ("", bobConn, SENT 4)
|
||||
bob <#= \case ("", c, Msg "hi") -> c == aliceConn; _ -> False
|
||||
bob #: ("13", aliceConn, "ACK 5") #> ("13", aliceConn, OK)
|
||||
bob #: ("13", aliceConn, "ACK 4") #> ("13", aliceConn, OK)
|
||||
|
||||
testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testRejectContactRequest _ alice bob = do
|
||||
@@ -254,20 +256,20 @@ testRejectContactRequest _ alice bob = do
|
||||
testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO ()
|
||||
testSubscription _ alice1 alice2 bob = do
|
||||
(alice1, "alice") `connect` (bob, "bob")
|
||||
bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 5)
|
||||
bob <# ("", "alice", SENT 5)
|
||||
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 5") #> ("1", "bob", OK)
|
||||
bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 6)
|
||||
bob <# ("", "alice", SENT 6)
|
||||
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 6") #> ("2", "bob", OK)
|
||||
alice1 #: ("2", "bob", "ACK 5") #> ("2", "bob", OK)
|
||||
alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK)
|
||||
alice1 <# ("", "bob", END)
|
||||
bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 7)
|
||||
bob <# ("", "alice", SENT 7)
|
||||
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 7") #> ("22", "bob", OK)
|
||||
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 ()
|
||||
@@ -283,23 +285,23 @@ testMsgDeliveryServerRestart :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testMsgDeliveryServerRestart t alice bob = do
|
||||
withServer $ do
|
||||
connect (alice, "alice") (bob, "bob")
|
||||
bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 5)
|
||||
bob <# ("", "alice", SENT 5)
|
||||
bob #: ("1", "alice", "SEND F 2\nhi") #> ("1", "alice", MID 4)
|
||||
bob <# ("", "alice", SENT 4)
|
||||
alice <#= \case ("", "bob", Msg "hi") -> True; _ -> False
|
||||
alice #: ("11", "bob", "ACK 5") #> ("11", "bob", OK)
|
||||
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 11\nhello again") #> ("2", "alice", MID 6)
|
||||
bob #: ("2", "alice", "SEND F 11\nhello again") #> ("2", "alice", MID 5)
|
||||
bob #:# "nothing else delivered before the server is restarted"
|
||||
alice #:# "nothing else delivered before the server is restarted"
|
||||
|
||||
withServer $ do
|
||||
bob <# ("", "alice", SENT 6)
|
||||
bob <# ("", "alice", SENT 5)
|
||||
alice <# ("", "", UP server ["bob"])
|
||||
alice <#= \case ("", "bob", Msg "hello again") -> True; _ -> False
|
||||
alice #: ("12", "bob", "ACK 6") #> ("12", "bob", OK)
|
||||
alice #: ("12", "bob", "ACK 5") #> ("12", "bob", OK)
|
||||
|
||||
removeFile testStoreLogFile
|
||||
where
|
||||
@@ -314,7 +316,7 @@ testServerConnectionAfterError t _ = do
|
||||
|
||||
bob <# ("", "", DOWN server ["alice"])
|
||||
alice <# ("", "", DOWN server ["bob"])
|
||||
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 5)
|
||||
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"
|
||||
|
||||
@@ -323,13 +325,13 @@ testServerConnectionAfterError t _ = do
|
||||
bob #: ("1", "alice", "SUB") #> ("1", "alice", ERR (BROKER NETWORK))
|
||||
alice #: ("1", "bob", "SUB") #> ("1", "bob", ERR (BROKER NETWORK))
|
||||
withServer $ do
|
||||
alice <# ("", "bob", SENT 5)
|
||||
alice <# ("", "bob", SENT 4)
|
||||
bob <# ("", "", UP server ["alice"])
|
||||
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
|
||||
bob #: ("2", "alice", "ACK 5") #> ("2", "alice", OK)
|
||||
bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK)
|
||||
alice <# ("", "", UP server ["bob"])
|
||||
alice #: ("1", "bob", "SEND 11\nhello again") #> ("1", "bob", MID 6)
|
||||
alice <# ("", "bob", SENT 6)
|
||||
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
|
||||
@@ -349,14 +351,14 @@ testMsgDeliveryAgentRestart t bob = do
|
||||
withAgent $ \alice -> do
|
||||
withServer $ do
|
||||
connect (bob, "bob") (alice, "alice")
|
||||
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 5)
|
||||
alice <# ("", "bob", SENT 5)
|
||||
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 5") #> ("11", "alice", OK)
|
||||
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 11\nhello again") #> ("2", "bob", MID 6)
|
||||
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"
|
||||
|
||||
@@ -366,11 +368,11 @@ testMsgDeliveryAgentRestart t bob = do
|
||||
alice <#= \case
|
||||
(corrId, "bob", cmd) ->
|
||||
(corrId == "3" && cmd == OK)
|
||||
|| (corrId == "" && cmd == SENT 6)
|
||||
|| (corrId == "" && cmd == SENT 5)
|
||||
_ -> False
|
||||
bob <# ("", "", UP server ["alice"])
|
||||
bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False
|
||||
bob #: ("12", "alice", "ACK 6") #> ("12", "alice", OK)
|
||||
bob #: ("12", "alice", "ACK 5") #> ("12", "alice", OK)
|
||||
|
||||
removeFile testStoreLogFile
|
||||
removeFile testDB
|
||||
@@ -394,15 +396,15 @@ testConcurrentMsgDelivery _ alice bob = do
|
||||
|
||||
-- the first connection should not be blocked by the second one
|
||||
sendMessage (alice, "alice") (bob, "bob") "hello"
|
||||
-- alice #: ("2", "bob", "SEND :hello") #> ("2", "bob", MID 1)
|
||||
-- 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 9\nhello too") #> ("14", "alice", MID 6)
|
||||
bob <# ("", "alice", SENT 6)
|
||||
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 6") #> ("3", "bob", OK)
|
||||
alice #: ("3", "bob", "ACK 5") #> ("3", "bob", OK)
|
||||
|
||||
testMsgDeliveryQuotaExceeded :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testMsgDeliveryQuotaExceeded _ alice bob = do
|
||||
@@ -411,13 +413,13 @@ testMsgDeliveryQuotaExceeded _ alice bob = do
|
||||
forM_ [1 .. 4 :: Int] $ \i -> do
|
||||
let corrId = bshow i
|
||||
msg = "message " <> bshow i
|
||||
(_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND :" <> msg)
|
||||
(_, "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 :over quota")
|
||||
(_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND F :over quota")
|
||||
|
||||
alice #: ("1", "bob2", "SEND :hello") #> ("1", "bob2", MID 5)
|
||||
alice #: ("1", "bob2", "SEND F :hello") #> ("1", "bob2", MID 4)
|
||||
-- if delivery is blocked it won't go further
|
||||
alice <# ("", "bob2", SENT 5)
|
||||
alice <# ("", "bob2", SENT 4)
|
||||
|
||||
connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO ()
|
||||
connect (h1, name1) (h2, name2) = do
|
||||
@@ -432,10 +434,10 @@ connect (h1, name1) (h2, name2) = do
|
||||
|
||||
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 :" <> msg)
|
||||
("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', 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
|
||||
|
||||
@@ -48,7 +48,7 @@ connReqData :: ConnReqUriData
|
||||
connReqData =
|
||||
ConnReqUriData
|
||||
{ crScheme = simplexChat,
|
||||
crAgentVRange = smpAgentVRange,
|
||||
crAgentVRange = mkVersionRange 1 1,
|
||||
crSmpQueues = [queue]
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ connectionRequest12 :: AConnectionRequestUri
|
||||
connectionRequest12 =
|
||||
ACR SCMInvitation $
|
||||
CRInvitationUri
|
||||
connReqData {crAgentVRange = mkVersionRange 1 2, crSmpQueues = [queue, queue]}
|
||||
connReqData {crAgentVRange = supportedSMPAgentVRange, crSmpQueues = [queue, queue]}
|
||||
testE2ERatchetParams13
|
||||
|
||||
connectionRequestTests :: Spec
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -6,21 +7,32 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module AgentTests.FunctionalAPITests (functionalAPITests) where
|
||||
module AgentTests.FunctionalAPITests
|
||||
( functionalAPITests,
|
||||
makeConnection,
|
||||
get,
|
||||
(##>),
|
||||
(=##>),
|
||||
pattern Msg,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad.Except (ExceptT, runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import SMPAgentClient
|
||||
import SMPClient (cfg, testPort, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn)
|
||||
import SMPClient (cfg, testPort, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..))
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..))
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (ATransport (..))
|
||||
import Simplex.Messaging.Version
|
||||
import Test.Hspec
|
||||
import UnliftIO
|
||||
|
||||
@@ -34,13 +46,36 @@ get :: MonadIO m => AgentClient -> m (ATransmission 'Agent)
|
||||
get c = atomically (readTBQueue $ subQ c)
|
||||
|
||||
pattern Msg :: MsgBody -> ACommand 'Agent
|
||||
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
|
||||
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} _ msgBody
|
||||
|
||||
smpCfgV1 :: ProtocolClientConfig
|
||||
smpCfgV1 = (smpCfg agentCfg) {smpServerVRange = mkVersionRange 1 1}
|
||||
|
||||
agentCfgV1 :: AgentConfig
|
||||
agentCfgV1 = agentCfg {smpAgentVersion = 1, smpAgentVRange = mkVersionRange 1 1, smpCfg = smpCfgV1}
|
||||
|
||||
functionalAPITests :: ATransport -> Spec
|
||||
functionalAPITests t = do
|
||||
describe "Establishing duplex connection" $
|
||||
it "should connect via one server using SMP agent clients" $
|
||||
withSmpServer t testAgentClient
|
||||
describe "Duplex connection between agent versions 1 and 2" $ do
|
||||
it "should connect agent v1 to v1" $
|
||||
withSmpServer t testAgentClientV1toV1
|
||||
it "should connect agent v1 to v2" $
|
||||
withSmpServer t testAgentClientV1toV2
|
||||
it "should connect agent v2 to v1" $
|
||||
withSmpServer t testAgentClientV2toV1
|
||||
describe "Establish duplex connection via contact address" $
|
||||
it "should connect via one server using SMP agent clients" $
|
||||
withSmpServer t testAgentClientContact
|
||||
describe "Duplex connection via contact address between agent versions 1 and 2" $ do
|
||||
it "should connect agent v1 to v1" $
|
||||
withSmpServer t testAgentClientContactV1toV1
|
||||
it "should connect agent v1 to v2" $
|
||||
withSmpServer t testAgentClientContactV1toV2
|
||||
it "should connect agent v2 to v1" $
|
||||
withSmpServer t testAgentClientContactV2toV1
|
||||
describe "Establishing connection asynchronously" $ do
|
||||
it "should connect with initiating client going offline" $
|
||||
withSmpServer t testAsyncInitiatingOffline
|
||||
@@ -52,16 +87,72 @@ functionalAPITests t = do
|
||||
testAsyncServerOffline t
|
||||
it "should notify after HELLO timeout" $
|
||||
withSmpServer t testAsyncHelloTimeout
|
||||
describe "Duplicate message delivery" $
|
||||
it "should deliver messages to the user once, even if repeat delivery is made by the server (no ACK)" $
|
||||
testDuplicateMessage t
|
||||
describe "Inactive client disconnection" $ do
|
||||
it "should disconnect clients if it was inactive longer than TTL" $
|
||||
testInactiveClientDisconnected t
|
||||
it "should NOT disconnect active clients" $
|
||||
testActiveClientNotDisconnected t
|
||||
describe "Suspending agent" $ do
|
||||
it "should update client when agent is suspended" $
|
||||
withSmpServer t testSuspendingAgent
|
||||
it "should complete sending messages when agent is suspended" $
|
||||
testSuspendingAgentCompleteSending t
|
||||
it "should suspend agent on timeout, even if pending messages not sent" $
|
||||
testSuspendingAgentTimeout t
|
||||
|
||||
testAgentClient :: IO ()
|
||||
testAgentClient = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
runAgentClientTest alice bob 3
|
||||
|
||||
testAgentClientV1toV1 :: IO ()
|
||||
testAgentClientV1toV1 = do
|
||||
alice <- getSMPAgentClient agentCfgV1 initAgentServers
|
||||
bob <- getSMPAgentClient agentCfgV1 {dbFile = testDB2} initAgentServers
|
||||
runAgentClientTest alice bob 4
|
||||
|
||||
testAgentClientV1toV2 :: IO ()
|
||||
testAgentClientV1toV2 = do
|
||||
alice <- getSMPAgentClient agentCfgV1 initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
runAgentClientTest alice bob 4
|
||||
|
||||
testAgentClientV2toV1 :: IO ()
|
||||
testAgentClientV2toV1 = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfgV1 {dbFile = testDB2} initAgentServers
|
||||
runAgentClientTest alice bob 4
|
||||
|
||||
testAgentClientContact :: IO ()
|
||||
testAgentClientContact = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
runAgentClientContactTest alice bob 3
|
||||
|
||||
testAgentClientContactV1toV1 :: IO ()
|
||||
testAgentClientContactV1toV1 = do
|
||||
alice <- getSMPAgentClient agentCfgV1 initAgentServers
|
||||
bob <- getSMPAgentClient agentCfgV1 {dbFile = testDB2} initAgentServers
|
||||
runAgentClientContactTest alice bob 4
|
||||
|
||||
testAgentClientContactV1toV2 :: IO ()
|
||||
testAgentClientContactV1toV2 = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfgV1 {dbFile = testDB2} initAgentServers
|
||||
runAgentClientContactTest alice bob 4
|
||||
|
||||
testAgentClientContactV2toV1 :: IO ()
|
||||
testAgentClientContactV2toV1 = do
|
||||
alice <- getSMPAgentClient agentCfgV1 initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
runAgentClientContactTest alice bob 4
|
||||
|
||||
runAgentClientTest :: AgentClient -> AgentClient -> AgentMsgId -> IO ()
|
||||
runAgentClientTest alice bob baseId = do
|
||||
Right () <- runExceptT $ do
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
@@ -70,37 +161,77 @@ testAgentClient = do
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get bob ##> ("", aliceId, CON)
|
||||
-- message IDs 1 to 4 get assigned to control messages, so first MSG is assigned ID 5
|
||||
5 <- sendMessage alice bobId "hello"
|
||||
get alice ##> ("", bobId, SENT 5)
|
||||
6 <- sendMessage alice bobId "how are you?"
|
||||
get alice ##> ("", bobId, SENT 6)
|
||||
-- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4
|
||||
1 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 1)
|
||||
2 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "how are you?"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 2)
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 5
|
||||
ackMessage bob aliceId $ baseId + 1
|
||||
get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 6
|
||||
7 <- sendMessage bob aliceId "hello too"
|
||||
get bob ##> ("", aliceId, SENT 7)
|
||||
8 <- sendMessage bob aliceId "message 1"
|
||||
get bob ##> ("", aliceId, SENT 8)
|
||||
ackMessage bob aliceId $ baseId + 2
|
||||
3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "hello too"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 3)
|
||||
4 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 1"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 4)
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 7
|
||||
ackMessage alice bobId $ baseId + 3
|
||||
get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 8
|
||||
ackMessage alice bobId $ baseId + 4
|
||||
suspendConnection alice bobId
|
||||
9 <- sendMessage bob aliceId "message 2"
|
||||
get bob ##> ("", aliceId, MERR 9 (SMP AUTH))
|
||||
5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2"
|
||||
get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH))
|
||||
deleteConnection alice bobId
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
pure ()
|
||||
where
|
||||
noMessages :: AgentClient -> String -> Expectation
|
||||
noMessages c err = tryGet `shouldReturn` ()
|
||||
where
|
||||
tryGet =
|
||||
10000 `timeout` get c >>= \case
|
||||
Just _ -> error err
|
||||
_ -> return ()
|
||||
msgId = subtract baseId
|
||||
|
||||
runAgentClientContactTest :: AgentClient -> AgentClient -> AgentMsgId -> IO ()
|
||||
runAgentClientContactTest alice bob baseId = do
|
||||
Right () <- runExceptT $ do
|
||||
(_, qInfo) <- createConnection alice SCMContact
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
("", _, REQ invId "bob's connInfo") <- get alice
|
||||
bobId <- acceptContact alice invId "alice's connInfo"
|
||||
("", _, CONF confId "alice's connInfo") <- get bob
|
||||
allowConnection bob aliceId confId "bob's connInfo"
|
||||
get alice ##> ("", bobId, INFO "bob's connInfo")
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, CON)
|
||||
-- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4
|
||||
1 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 1)
|
||||
2 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "how are you?"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 2)
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId $ baseId + 1
|
||||
get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId $ baseId + 2
|
||||
3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "hello too"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 3)
|
||||
4 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 1"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 4)
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 3
|
||||
get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 4
|
||||
suspendConnection alice bobId
|
||||
5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2"
|
||||
get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH))
|
||||
deleteConnection alice bobId
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
pure ()
|
||||
where
|
||||
msgId = subtract baseId
|
||||
|
||||
noMessages :: AgentClient -> String -> Expectation
|
||||
noMessages c err = tryGet `shouldReturn` ()
|
||||
where
|
||||
tryGet =
|
||||
10000 `timeout` get c >>= \case
|
||||
Just _ -> error err
|
||||
_ -> return ()
|
||||
|
||||
testAsyncInitiatingOffline :: IO ()
|
||||
testAsyncInitiatingOffline = do
|
||||
@@ -188,7 +319,8 @@ testAsyncServerOffline t = do
|
||||
|
||||
testAsyncHelloTimeout :: IO ()
|
||||
testAsyncHelloTimeout = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
-- this test would only work if any of the agent is v1, there is no HELLO timeout in v2
|
||||
alice <- getSMPAgentClient agentCfgV1 initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2, helloTimeout = 1} initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
(_, cReq) <- createConnection alice SCMInvitation
|
||||
@@ -197,6 +329,66 @@ testAsyncHelloTimeout = do
|
||||
get bob ##> ("", aliceId, ERR $ CONN NOT_ACCEPTED)
|
||||
pure ()
|
||||
|
||||
testDuplicateMessage :: ATransport -> IO ()
|
||||
testDuplicateMessage t = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
(aliceId, bobId, bob1) <- withSmpServerStoreMsgLogOn t testPort $ \_ -> do
|
||||
Right (aliceId, bobId) <- runExceptT $ makeConnection alice bob
|
||||
Right () <- runExceptT $ do
|
||||
4 <- sendMessage alice bobId SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT 4)
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
disconnectAgentClient bob
|
||||
|
||||
-- if the agent user did not send ACK, the message will be delivered again
|
||||
bob1 <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
subscribeConnection bob1 aliceId
|
||||
get bob1 =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob1 aliceId 4
|
||||
5 <- sendMessage alice bobId SMP.noMsgFlags "hello 2"
|
||||
get alice ##> ("", bobId, SENT 5)
|
||||
get bob1 =##> \case ("", c, Msg "hello 2") -> c == aliceId; _ -> False
|
||||
|
||||
pure (aliceId, bobId, bob1)
|
||||
|
||||
get alice =##> \case ("", "", DOWN _ [c]) -> c == bobId; _ -> False
|
||||
get bob1 =##> \case ("", "", DOWN _ [c]) -> c == aliceId; _ -> False
|
||||
-- commenting two lines below and uncommenting further two lines would also pass,
|
||||
-- it is the scenario tested above, when the message was not acknowledged by the user
|
||||
threadDelay 200000
|
||||
Left (BROKER TIMEOUT) <- runExceptT $ ackMessage bob1 aliceId 5
|
||||
|
||||
disconnectAgentClient alice
|
||||
disconnectAgentClient bob1
|
||||
|
||||
alice2 <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob2 <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
|
||||
withSmpServerStoreMsgLogOn t testPort $ \_ -> do
|
||||
Right () <- runExceptT $ do
|
||||
subscribeConnection bob2 aliceId
|
||||
subscribeConnection alice2 bobId
|
||||
-- get bob2 =##> \case ("", c, Msg "hello 2") -> c == aliceId; _ -> False
|
||||
-- ackMessage bob2 aliceId 5
|
||||
-- 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)
|
||||
get bob2 =##> \case ("", c, Msg "hello 3") -> c == aliceId; _ -> False
|
||||
pure ()
|
||||
|
||||
makeConnection :: AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnection alice bob = do
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
("", _, CONF confId "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get bob ##> ("", aliceId, CON)
|
||||
pure (aliceId, bobId)
|
||||
|
||||
testInactiveClientDisconnected :: ATransport -> IO ()
|
||||
testInactiveClientDisconnected t = do
|
||||
let cfg' = cfg {inactiveClientExpiration = Just ExpirationConfig {ttl = 1, checkInterval = 1}}
|
||||
@@ -235,13 +427,89 @@ testActiveClientNotDisconnected t = do
|
||||
get alice ##> ("", "", DOWN testSMPServer [connId])
|
||||
milliseconds ts = systemSeconds ts * 1000 + fromIntegral (systemNanoseconds ts `div` 1000000)
|
||||
|
||||
testSuspendingAgent :: IO ()
|
||||
testSuspendingAgent = do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
b <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
(aId, bId) <- makeConnection a b
|
||||
4 <- sendMessage a bId SMP.noMsgFlags "hello"
|
||||
get a ##> ("", bId, SENT 4)
|
||||
get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False
|
||||
ackMessage b aId 4
|
||||
suspendAgent b 1000000
|
||||
get b ##> ("", "", SUSPENDED)
|
||||
5 <- sendMessage a bId SMP.noMsgFlags "hello 2"
|
||||
get a ##> ("", bId, SENT 5)
|
||||
Nothing <- 100000 `timeout` get b
|
||||
activateAgent b
|
||||
get b =##> \case ("", c, Msg "hello 2") -> c == aId; _ -> False
|
||||
pure ()
|
||||
|
||||
testSuspendingAgentCompleteSending :: ATransport -> IO ()
|
||||
testSuspendingAgentCompleteSending t = do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
b <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runExceptT $ do
|
||||
(aId, bId) <- makeConnection a b
|
||||
4 <- sendMessage a bId SMP.noMsgFlags "hello"
|
||||
get a ##> ("", bId, SENT 4)
|
||||
get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False
|
||||
ackMessage b aId 4
|
||||
pure (aId, bId)
|
||||
|
||||
Right () <- runExceptT $ do
|
||||
("", "", DOWN {}) <- get a
|
||||
("", "", DOWN {}) <- get b
|
||||
5 <- sendMessage b aId SMP.noMsgFlags "hello too"
|
||||
6 <- sendMessage b aId SMP.noMsgFlags "how are you?"
|
||||
liftIO $ threadDelay 100000
|
||||
suspendAgent b 5000000
|
||||
|
||||
Right () <- withSmpServerStoreLogOn t testPort $ \_ -> runExceptT $ do
|
||||
get b =##> \case ("", c, SENT 5) -> c == aId; ("", "", UP {}) -> True; _ -> False
|
||||
get b =##> \case ("", c, SENT 5) -> c == aId; ("", "", UP {}) -> True; _ -> False
|
||||
get b =##> \case ("", c, SENT 6) -> c == aId; _ -> False
|
||||
("", "", SUSPENDED) <- get b
|
||||
|
||||
("", "", UP {}) <- get a
|
||||
get a =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False
|
||||
ackMessage a bId 5
|
||||
get a =##> \case ("", c, Msg "how are you?") -> c == bId; _ -> False
|
||||
ackMessage a bId 6
|
||||
|
||||
pure ()
|
||||
|
||||
testSuspendingAgentTimeout :: ATransport -> IO ()
|
||||
testSuspendingAgentTimeout t = do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
b <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right (aId, _) <- withSmpServer t . runExceptT $ do
|
||||
(aId, bId) <- makeConnection a b
|
||||
4 <- sendMessage a bId SMP.noMsgFlags "hello"
|
||||
get a ##> ("", bId, SENT 4)
|
||||
get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False
|
||||
ackMessage b aId 4
|
||||
pure (aId, bId)
|
||||
|
||||
Right () <- runExceptT $ do
|
||||
("", "", DOWN {}) <- get a
|
||||
("", "", DOWN {}) <- get b
|
||||
5 <- sendMessage b aId SMP.noMsgFlags "hello too"
|
||||
6 <- sendMessage b aId SMP.noMsgFlags "how are you?"
|
||||
suspendAgent b 100000
|
||||
("", "", SUSPENDED) <- get b
|
||||
pure ()
|
||||
|
||||
pure ()
|
||||
|
||||
exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
|
||||
exchangeGreetings alice bobId bob aliceId = do
|
||||
5 <- sendMessage alice bobId "hello"
|
||||
get alice ##> ("", bobId, SENT 5)
|
||||
4 <- sendMessage alice bobId SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT 4)
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 5
|
||||
6 <- sendMessage bob aliceId "hello too"
|
||||
get bob ##> ("", aliceId, SENT 6)
|
||||
ackMessage bob aliceId 4
|
||||
5 <- sendMessage bob aliceId SMP.noMsgFlags "hello too"
|
||||
get bob ##> ("", aliceId, SENT 5)
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 6
|
||||
ackMessage alice bobId 5
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module AgentTests.NotificationTests where
|
||||
|
||||
-- import Control.Logger.Simple (LogConfig (..), LogLevel (..), setLogLevel, withGlobalLogging)
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import AgentTests.FunctionalAPITests (get, makeConnection, (##>), (=##>), pattern Msg)
|
||||
import Control.Concurrent (killThread, threadDelay)
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Types as JT
|
||||
@@ -15,22 +20,29 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import NtfClient
|
||||
import SMPAgentClient (agentCfg, initAgentServers, testDB, testDB2)
|
||||
import SMPClient (withSmpServer)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Protocol (ErrorType (AUTH))
|
||||
import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), SMPMsgMeta (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport (ATransport)
|
||||
import Simplex.Messaging.Util (tryE)
|
||||
import System.Directory (removeFile)
|
||||
import System.Directory (doesFileExist, removeFile)
|
||||
import Test.Hspec
|
||||
import UnliftIO.STM
|
||||
import UnliftIO
|
||||
|
||||
removeFileIfExists :: FilePath -> IO ()
|
||||
removeFileIfExists filePath = do
|
||||
fileExists <- doesFileExist filePath
|
||||
when fileExists $ removeFile filePath
|
||||
|
||||
notificationTests :: ATransport -> Spec
|
||||
notificationTests t =
|
||||
after_ (removeFile testDB) $
|
||||
after_ (removeFile testDB >> removeFileIfExists testDB2) $ do
|
||||
describe "Managing notification tokens" $ do
|
||||
it "should register and verify notification token" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
@@ -44,20 +56,37 @@ notificationTests t =
|
||||
it "should re-register token when notification server is restarted" $ \_ ->
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenServerRestart t apns
|
||||
describe "Managing notification subscriptions" $ do
|
||||
it "should create notification subscription for existing connection" $ \_ ->
|
||||
withSmpServer t $
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testNotificationSubscriptionExistingConnection apns
|
||||
it "should create notification subscription for new connection" $ \_ ->
|
||||
withSmpServer t $
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testNotificationSubscriptionNewConnection apns
|
||||
it "should change notifications mode" $ \_ ->
|
||||
withSmpServer t $
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testChangeNotificationsMode apns
|
||||
describe "Notifications server store log" $ do
|
||||
it "should save and restore tokens and subscriptions" $ \_ ->
|
||||
withSmpServer t $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNotificationsStoreLog t apns
|
||||
|
||||
testNotificationToken :: APNSMockServer -> IO ()
|
||||
testNotificationToken APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
let tkn = DeviceToken PPApns "abcd"
|
||||
NTRegistered <- registerNtfToken a tkn
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
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 verification nonce
|
||||
enableNtfCron a tkn 30
|
||||
verifyNtfToken a tkn nonce verification
|
||||
NTActive <- checkNtfToken a tkn
|
||||
deleteNtfToken a tkn
|
||||
-- agent deleted this token
|
||||
@@ -79,22 +108,21 @@ testNtfTokenRepeatRegistration APNSMockServer {apnsQ} = do
|
||||
-- withGlobalLogging logCfg $ do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
let tkn = DeviceToken PPApns "abcd"
|
||||
NTRegistered <- registerNtfToken a tkn
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
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
|
||||
NTRegistered <- registerNtfToken a tkn
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
_ <- ntfData' .-> "verification"
|
||||
_ <- C.cbNonce <$> ntfData' .-> "nonce"
|
||||
liftIO $ sendApnsResponse' APNSRespOk
|
||||
-- can still use the first verification code, it is the same after decryption
|
||||
verifyNtfToken a tkn verification nonce
|
||||
enableNtfCron a tkn 30
|
||||
verifyNtfToken a tkn nonce verification
|
||||
NTActive <- checkNtfToken a tkn
|
||||
pure ()
|
||||
pure ()
|
||||
@@ -106,16 +134,16 @@ testNtfTokenSecondRegistration APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
a' <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
let tkn = DeviceToken PPApns "abcd"
|
||||
NTRegistered <- registerNtfToken a tkn
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
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 verification nonce
|
||||
verifyNtfToken a tkn nonce verification
|
||||
|
||||
NTRegistered <- registerNtfToken a' tkn
|
||||
NTRegistered <- registerNtfToken a' tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification' <- ntfData' .-> "verification"
|
||||
@@ -125,23 +153,23 @@ testNtfTokenSecondRegistration APNSMockServer {apnsQ} = do
|
||||
-- at this point the first token is still active
|
||||
NTActive <- checkNtfToken a tkn
|
||||
-- and the second is not yet verified
|
||||
liftIO $ threadDelay 50000
|
||||
NTConfirmed <- checkNtfToken a' tkn
|
||||
-- now the second token registration is verified
|
||||
verifyNtfToken a' tkn verification' nonce'
|
||||
verifyNtfToken a' tkn nonce' verification'
|
||||
-- the first registration is removed
|
||||
Left (NTF AUTH) <- tryE $ checkNtfToken a tkn
|
||||
-- and the second is active
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
enableNtfCron a' tkn 30
|
||||
pure ()
|
||||
pure ()
|
||||
|
||||
testNtfTokenServerRestart :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient agentCfg initAgentServers
|
||||
let tkn = DeviceToken PPApns "abcd"
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
Right ntfData <- withNtfServer t . runExceptT $ do
|
||||
NTRegistered <- registerNtfToken a tkn
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
@@ -155,13 +183,213 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
Right () <- withNtfServer t . runExceptT $ do
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
Left (NTF AUTH) <- tryE $ verifyNtfToken a' tkn verification nonce
|
||||
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"
|
||||
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
|
||||
liftIO $ sendApnsResponse' APNSRespOk
|
||||
verifyNtfToken a' tkn verification' nonce'
|
||||
verifyNtfToken a' tkn nonce' verification'
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
enableNtfCron a' tkn 30
|
||||
pure ()
|
||||
pure ()
|
||||
|
||||
testNotificationSubscriptionExistingConnection :: APNSMockServer -> IO ()
|
||||
testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right (bobId, aliceId, nonce, message) <- runExceptT $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
("", _, CONF confId "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, CON)
|
||||
-- register notification token
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
NTRegistered <- registerNtfToken alice tkn NMInstant
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification <- ntfData .-> "verification"
|
||||
vNonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
verifyNtfToken alice tkn vNonce verification
|
||||
NTActive <- checkNtfToken alice tkn
|
||||
-- send message
|
||||
liftIO $ threadDelay 50000
|
||||
1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 1)
|
||||
-- notification
|
||||
(nonce, message) <- messageNotification apnsQ
|
||||
pure (bobId, aliceId, nonce, message)
|
||||
|
||||
-- alice client already has subscription for the connection
|
||||
Left (CMD PROHIBITED) <- runExceptT $ getNotificationMessage alice nonce message
|
||||
|
||||
-- aliceNtf client doesn't have subscription and is allowed to get notification message
|
||||
aliceNtf <- getSMPAgentClient agentCfg initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
(_, [SMPMsgMeta {msgFlags = MsgFlags True}]) <- getNotificationMessage aliceNtf nonce message
|
||||
pure ()
|
||||
disconnectAgentClient aliceNtf
|
||||
|
||||
Right () <- runExceptT $ do
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 1
|
||||
-- delete notification subscription
|
||||
deleteNtfSub alice bobId
|
||||
-- send message
|
||||
2 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 2)
|
||||
-- no notifications should follow
|
||||
500000 `timeout` atomically (readTBQueue apnsQ) >>= \case
|
||||
Nothing -> pure ()
|
||||
_ -> error "unexpected notification"
|
||||
pure ()
|
||||
where
|
||||
baseId = 3
|
||||
msgId = subtract baseId
|
||||
|
||||
testNotificationSubscriptionNewConnection :: APNSMockServer -> IO ()
|
||||
testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
-- alice registers notification token
|
||||
_ <- registerTestToken alice "abcd" NMInstant apnsQ
|
||||
-- bob registers notification token
|
||||
_ <- registerTestToken bob "bcde" NMInstant apnsQ
|
||||
-- establish connection
|
||||
liftIO $ threadDelay 50000
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
liftIO $ threadDelay 500000
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
void $ messageNotification apnsQ
|
||||
("", _, CONF confId "bob's connInfo") <- get alice
|
||||
liftIO $ threadDelay 500000
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
void $ messageNotification apnsQ
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
void $ messageNotification apnsQ
|
||||
get alice ##> ("", bobId, CON)
|
||||
void $ messageNotification apnsQ
|
||||
get bob ##> ("", aliceId, CON)
|
||||
-- bob sends message
|
||||
1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 1)
|
||||
void $ messageNotification apnsQ
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 1
|
||||
-- alice sends message
|
||||
2 <- msgId <$> sendMessage alice bobId (SMP.MsgFlags True) "hey there"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 2)
|
||||
void $ messageNotification apnsQ
|
||||
get bob =##> \case ("", c, Msg "hey there") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId $ baseId + 2
|
||||
-- no unexpected notifications should follow
|
||||
500000 `timeout` atomically (readTBQueue apnsQ) >>= \case
|
||||
Nothing -> pure ()
|
||||
_ -> error "unexpected notification"
|
||||
pure ()
|
||||
where
|
||||
baseId = 3
|
||||
msgId = subtract baseId
|
||||
|
||||
registerTestToken :: AgentClient -> ByteString -> NotificationsMode -> TBQueue APNSMockRequest -> ExceptT AgentErrorType IO DeviceToken
|
||||
registerTestToken a token mode apnsQ = do
|
||||
let tkn = DeviceToken PPApnsTest token
|
||||
NTRegistered <- registerNtfToken a tkn mode
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification' <- ntfData' .-> "verification"
|
||||
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
|
||||
liftIO $ sendApnsResponse' APNSRespOk
|
||||
verifyNtfToken a tkn nonce' verification'
|
||||
NTActive <- checkNtfToken a tkn
|
||||
pure tkn
|
||||
|
||||
testChangeNotificationsMode :: APNSMockServer -> IO ()
|
||||
testChangeNotificationsMode APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right () <- runExceptT $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
("", _, CONF confId "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, CON)
|
||||
-- register notification token, set mode to NMPeriodic
|
||||
tkn <- registerTestToken alice "abcd" NMPeriodic apnsQ
|
||||
-- send message, no notification
|
||||
1 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 1)
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 1
|
||||
-- set mode to NMInstant
|
||||
NTActive <- registerNtfToken alice tkn NMInstant
|
||||
-- send message, receive notification
|
||||
liftIO $ threadDelay 500000
|
||||
2 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 2)
|
||||
void $ messageNotification apnsQ
|
||||
get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 2
|
||||
-- reset mode to NMPeriodic
|
||||
NTActive <- registerNtfToken alice tkn NMPeriodic
|
||||
-- send message, no notification
|
||||
liftIO $ threadDelay 500000
|
||||
3 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello there"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 3)
|
||||
get alice =##> \case ("", c, Msg "hello there") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId $ baseId + 3
|
||||
-- no notifications should follow
|
||||
500000 `timeout` atomically (readTBQueue apnsQ) >>= \case
|
||||
Nothing -> pure ()
|
||||
_ -> error "unexpected notification"
|
||||
pure ()
|
||||
where
|
||||
baseId = 3
|
||||
msgId = subtract baseId
|
||||
|
||||
testNotificationsStoreLog :: ATransport -> APNSMockServer -> IO ()
|
||||
testNotificationsStoreLog t APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient agentCfg initAgentServers
|
||||
bob <- getSMPAgentClient agentCfg {dbFile = testDB2} initAgentServers
|
||||
Right (aliceId, bobId) <- withNtfServerStoreLog t $ \threadId -> runExceptT $ do
|
||||
_ <- registerTestToken alice "abcd" NMInstant apnsQ
|
||||
(aliceId, bobId) <- makeConnection alice bob
|
||||
liftIO $ threadDelay 250000
|
||||
4 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello"
|
||||
get bob ##> ("", aliceId, SENT 4)
|
||||
void $ messageNotification apnsQ
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 4
|
||||
liftIO $ killThread threadId
|
||||
pure (aliceId, bobId)
|
||||
|
||||
liftIO $ threadDelay 250000
|
||||
|
||||
Right () <- withNtfServerStoreLog t $ \threadId -> runExceptT $ do
|
||||
liftIO $ threadDelay 250000
|
||||
5 <- sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
|
||||
get bob ##> ("", aliceId, SENT 5)
|
||||
void $ messageNotification apnsQ
|
||||
get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
|
||||
liftIO $ killThread threadId
|
||||
pure ()
|
||||
|
||||
messageNotification :: TBQueue APNSMockRequest -> ExceptT AgentErrorType IO (C.CbNonce, ByteString)
|
||||
messageNotification apnsQ = do
|
||||
500000 `timeout` atomically (readTBQueue apnsQ) >>= \case
|
||||
Nothing -> error "no notification"
|
||||
Just APNSMockRequest {notification = APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData}, sendApnsResponse} -> do
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
message <- ntfData .-> "message"
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
pure (nonce, message)
|
||||
_ -> error "bad notification"
|
||||
|
||||
+175
-170
@@ -11,7 +11,6 @@ module AgentTests.SQLiteTests (storeTests) where
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (replicateM_)
|
||||
import Control.Monad.Except (ExceptT, runExceptT)
|
||||
import Crypto.Random (drgNew)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.Text as T
|
||||
@@ -27,6 +26,7 @@ import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import System.Random
|
||||
import Test.Hspec
|
||||
import UnliftIO.Directory (removeFile)
|
||||
@@ -43,7 +43,7 @@ withStore2 = before connect2 . after (removeStore . fst)
|
||||
connect2 :: IO (SQLiteStore, SQLiteStore)
|
||||
connect2 = do
|
||||
s1 <- createStore
|
||||
s2 <- connectSQLiteStore (dbFilePath s1) 4
|
||||
s2 <- connectSQLiteStore (dbFilePath s1)
|
||||
pure (s1, s2)
|
||||
|
||||
createStore :: IO SQLiteStore
|
||||
@@ -51,21 +51,15 @@ createStore = do
|
||||
-- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous
|
||||
-- IO operations on multiple similarly named files; error seems to be environment specific
|
||||
r <- randomIO :: IO Word32
|
||||
createSQLiteStore (testDB <> show r) 4 Migrations.app True
|
||||
createSQLiteStore (testDB <> show r) Migrations.app True
|
||||
|
||||
removeStore :: SQLiteStore -> IO ()
|
||||
removeStore store = do
|
||||
close store
|
||||
removeFile $ dbFilePath store
|
||||
removeStore db = do
|
||||
close db
|
||||
removeFile $ dbFilePath db
|
||||
where
|
||||
close :: SQLiteStore -> IO ()
|
||||
close st = mapM_ DB.close =<< atomically (flushTBQueue $ dbConnPool st)
|
||||
|
||||
returnsResult :: (Eq a, Eq e, Show a, Show e) => ExceptT e IO a -> a -> Expectation
|
||||
action `returnsResult` r = runExceptT action `shouldReturn` Right r
|
||||
|
||||
throwsError :: (Eq a, Eq e, Show a, Show e) => ExceptT e IO a -> e -> Expectation
|
||||
action `throwsError` e = runExceptT action `shouldReturn` Left e
|
||||
close st = mapM_ DB.close =<< atomically (tryTakeTMVar $ dbConnection st)
|
||||
|
||||
-- TODO add null port tests
|
||||
storeTests :: Spec
|
||||
@@ -73,10 +67,10 @@ storeTests = do
|
||||
withStore2 $ do
|
||||
describe "stress test" testConcurrentWrites
|
||||
withStore $ do
|
||||
describe "store setup" $ do
|
||||
describe "db setup" $ do
|
||||
testCompiledThreadsafe
|
||||
testForeignKeysEnabled
|
||||
describe "store methods" $ do
|
||||
describe "db methods" $ do
|
||||
describe "Queue and Connection management" $ do
|
||||
describe "createRcvConn" $ do
|
||||
testCreateRcvConn
|
||||
@@ -111,28 +105,29 @@ testConcurrentWrites :: SpecWith (SQLiteStore, SQLiteStore)
|
||||
testConcurrentWrites =
|
||||
it "should complete multiple concurrent write transactions w/t sqlite busy errors" $ \(s1, s2) -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn s1 g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- withTransaction s1 $ \db ->
|
||||
createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
let ConnData {connId} = cData1
|
||||
concurrently_ (runTest s1 connId) (runTest s2 connId)
|
||||
where
|
||||
runTest :: SQLiteStore -> ConnId -> IO (Either StoreError ())
|
||||
runTest store connId = runExceptT . replicateM_ 100 $ do
|
||||
(internalId, internalRcvId, _, _) <- updateRcvIds store connId
|
||||
runTest :: SQLiteStore -> ConnId -> IO ()
|
||||
runTest st connId = replicateM_ 100 . withTransaction st $ \db -> do
|
||||
(internalId, internalRcvId, _, _) <- updateRcvIds db connId
|
||||
let rcvMsgData = mkRcvMsgData internalId internalRcvId 0 "0" "hash_dummy"
|
||||
createRcvMsg store connId rcvMsgData
|
||||
createRcvMsg db connId rcvMsgData
|
||||
|
||||
testCompiledThreadsafe :: SpecWith SQLiteStore
|
||||
testCompiledThreadsafe =
|
||||
it "compiled sqlite library should be threadsafe" . withStoreConnection $ \db -> do
|
||||
it "compiled sqlite library should be threadsafe" . withStoreTransaction $ \db -> do
|
||||
compileOptions <- DB.query_ db "pragma COMPILE_OPTIONS;" :: IO [[T.Text]]
|
||||
compileOptions `shouldNotContain` [["THREADSAFE=0"]]
|
||||
|
||||
withStoreConnection :: (DB.Connection -> IO a) -> SQLiteStore -> IO a
|
||||
withStoreConnection = flip withConnection
|
||||
withStoreTransaction :: (DB.Connection -> IO a) -> SQLiteStore -> IO a
|
||||
withStoreTransaction = flip withTransaction
|
||||
|
||||
testForeignKeysEnabled :: SpecWith SQLiteStore
|
||||
testForeignKeysEnabled =
|
||||
it "foreign keys should be enabled" . withStoreConnection $ \db -> do
|
||||
it "foreign keys should be enabled" . withStoreTransaction $ \db -> do
|
||||
let inconsistentQuery =
|
||||
[sql|
|
||||
INSERT INTO snd_queues
|
||||
@@ -144,7 +139,7 @@ testForeignKeysEnabled =
|
||||
`shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint)
|
||||
|
||||
cData1 :: ConnData
|
||||
cData1 = ConnData {connId = "conn1"}
|
||||
cData1 = ConnData {connId = "conn1", connAgentVersion = 1, duplexHandshake = Nothing}
|
||||
|
||||
testPrivateSignKey :: C.APrivateSignKey
|
||||
testPrivateSignKey = C.APrivateSignKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe"
|
||||
@@ -165,7 +160,8 @@ rcvQueue1 =
|
||||
e2ePrivKey = testPrivDhKey,
|
||||
e2eDhSecret = Nothing,
|
||||
sndId = Just "2345",
|
||||
status = New
|
||||
status = New,
|
||||
clientNtfCreds = Nothing
|
||||
}
|
||||
|
||||
sndQueue1 :: SndQueue
|
||||
@@ -182,125 +178,125 @@ sndQueue1 =
|
||||
|
||||
testCreateRcvConn :: SpecWith SQLiteStore
|
||||
testCreateRcvConn =
|
||||
it "should create RcvConnection and add SndQueue" $ \store -> do
|
||||
it "should create RcvConnection and add SndQueue" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
`returnsResult` "conn1"
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
`shouldReturn` Right "conn1"
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rcvQueue1))
|
||||
upgradeRcvConnToDuplex db "conn1" sndQueue1
|
||||
`shouldReturn` Right ()
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1))
|
||||
|
||||
testCreateRcvConnRandomId :: SpecWith SQLiteStore
|
||||
testCreateRcvConnRandomId =
|
||||
it "should create RcvConnection and add SndQueue with random ID" $ \store -> do
|
||||
it "should create RcvConnection and add SndQueue with random ID" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
Right connId <- runExceptT $ createRcvConn store g cData1 {connId = ""} rcvQueue1 SCMInvitation
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 {connId} rcvQueue1)
|
||||
upgradeRcvConnToDuplex store connId sndQueue1
|
||||
`returnsResult` ()
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1)
|
||||
Right connId <- createRcvConn db g cData1 {connId = ""} rcvQueue1 SCMInvitation
|
||||
getConn db connId
|
||||
`shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 {connId} rcvQueue1))
|
||||
upgradeRcvConnToDuplex db connId sndQueue1
|
||||
`shouldReturn` Right ()
|
||||
getConn db connId
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1))
|
||||
|
||||
testCreateRcvConnDuplicate :: SpecWith SQLiteStore
|
||||
testCreateRcvConnDuplicate =
|
||||
it "should throw error on attempt to create duplicate RcvConnection" $ \store -> do
|
||||
it "should throw error on attempt to create duplicate RcvConnection" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
`throwsError` SEConnDuplicate
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
`shouldReturn` Left (SEConnDuplicate)
|
||||
|
||||
testCreateSndConn :: SpecWith SQLiteStore
|
||||
testCreateSndConn =
|
||||
it "should create SndConnection and add RcvQueue" $ \store -> do
|
||||
it "should create SndConnection and add RcvQueue" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
createSndConn store g cData1 sndQueue1
|
||||
`returnsResult` "conn1"
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
|
||||
upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
createSndConn db g cData1 sndQueue1
|
||||
`shouldReturn` Right "conn1"
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sndQueue1))
|
||||
upgradeSndConnToDuplex db "conn1" rcvQueue1
|
||||
`shouldReturn` Right ()
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1))
|
||||
|
||||
testCreateSndConnRandomID :: SpecWith SQLiteStore
|
||||
testCreateSndConnRandomID =
|
||||
it "should create SndConnection and add RcvQueue with random ID" $ \store -> do
|
||||
it "should create SndConnection and add RcvQueue with random ID" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
Right connId <- runExceptT $ createSndConn store g cData1 {connId = ""} sndQueue1
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 {connId} sndQueue1)
|
||||
upgradeSndConnToDuplex store connId rcvQueue1
|
||||
`returnsResult` ()
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1)
|
||||
Right connId <- createSndConn db g cData1 {connId = ""} sndQueue1
|
||||
getConn db connId
|
||||
`shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 {connId} sndQueue1))
|
||||
upgradeSndConnToDuplex db connId rcvQueue1
|
||||
`shouldReturn` Right ()
|
||||
getConn db connId
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1))
|
||||
|
||||
testCreateSndConnDuplicate :: SpecWith SQLiteStore
|
||||
testCreateSndConnDuplicate =
|
||||
it "should throw error on attempt to create duplicate SndConnection" $ \store -> do
|
||||
it "should throw error on attempt to create duplicate SndConnection" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
createSndConn store g cData1 sndQueue1
|
||||
`throwsError` SEConnDuplicate
|
||||
_ <- createSndConn db g cData1 sndQueue1
|
||||
createSndConn db g cData1 sndQueue1
|
||||
`shouldReturn` Left (SEConnDuplicate)
|
||||
|
||||
testGetRcvConn :: SpecWith SQLiteStore
|
||||
testGetRcvConn =
|
||||
it "should get connection using rcv queue id and server" $ \store -> do
|
||||
it "should get connection using rcv queue id and server" . withStoreTransaction $ \db -> do
|
||||
let smpServer = SMPServer "smp.simplex.im" "5223" testKeyHash
|
||||
let recipientId = "1234"
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
getRcvConn store smpServer recipientId
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
getRcvConn db smpServer recipientId
|
||||
`shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rcvQueue1))
|
||||
|
||||
testDeleteRcvConn :: SpecWith SQLiteStore
|
||||
testDeleteRcvConn =
|
||||
it "should create RcvConnection and delete it" $ \store -> do
|
||||
it "should create RcvConnection and delete it" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rcvQueue1))
|
||||
deleteConn db "conn1"
|
||||
`shouldReturn` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEConnNotFound
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Left (SEConnNotFound)
|
||||
|
||||
testDeleteSndConn :: SpecWith SQLiteStore
|
||||
testDeleteSndConn =
|
||||
it "should create SndConnection and delete it" $ \store -> do
|
||||
it "should create SndConnection and delete it" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
_ <- createSndConn db g cData1 sndQueue1
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sndQueue1))
|
||||
deleteConn db "conn1"
|
||||
`shouldReturn` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEConnNotFound
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Left (SEConnNotFound)
|
||||
|
||||
testDeleteDuplexConn :: SpecWith SQLiteStore
|
||||
testDeleteDuplexConn =
|
||||
it "should create DuplexConnection and delete it" $ \store -> do
|
||||
it "should create DuplexConnection and delete it" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- upgradeRcvConnToDuplex db "conn1" sndQueue1
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1))
|
||||
deleteConn db "conn1"
|
||||
`shouldReturn` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEConnNotFound
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Left (SEConnNotFound)
|
||||
|
||||
testUpgradeRcvConnToDuplex :: SpecWith SQLiteStore
|
||||
testUpgradeRcvConnToDuplex =
|
||||
it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" $ \store -> do
|
||||
it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
_ <- createSndConn db g cData1 sndQueue1
|
||||
let anotherSndQueue =
|
||||
SndQueue
|
||||
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
|
||||
@@ -311,17 +307,17 @@ testUpgradeRcvConnToDuplex =
|
||||
e2eDhSecret = testDhSecret,
|
||||
status = New
|
||||
}
|
||||
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
|
||||
`throwsError` SEBadConnType CSnd
|
||||
_ <- runExceptT $ upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
|
||||
`throwsError` SEBadConnType CDuplex
|
||||
upgradeRcvConnToDuplex db "conn1" anotherSndQueue
|
||||
`shouldReturn` Left (SEBadConnType CSnd)
|
||||
_ <- upgradeSndConnToDuplex db "conn1" rcvQueue1
|
||||
upgradeRcvConnToDuplex db "conn1" anotherSndQueue
|
||||
`shouldReturn` Left (SEBadConnType CDuplex)
|
||||
|
||||
testUpgradeSndConnToDuplex :: SpecWith SQLiteStore
|
||||
testUpgradeSndConnToDuplex =
|
||||
it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" $ \store -> do
|
||||
it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
let anotherRcvQueue =
|
||||
RcvQueue
|
||||
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
|
||||
@@ -331,54 +327,55 @@ testUpgradeSndConnToDuplex =
|
||||
e2ePrivKey = testPrivDhKey,
|
||||
e2eDhSecret = Nothing,
|
||||
sndId = Just "4567",
|
||||
status = New
|
||||
status = New,
|
||||
clientNtfCreds = Nothing
|
||||
}
|
||||
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
|
||||
`throwsError` SEBadConnType CRcv
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
|
||||
`throwsError` SEBadConnType CDuplex
|
||||
upgradeSndConnToDuplex db "conn1" anotherRcvQueue
|
||||
`shouldReturn` Left (SEBadConnType CRcv)
|
||||
_ <- upgradeRcvConnToDuplex db "conn1" sndQueue1
|
||||
upgradeSndConnToDuplex db "conn1" anotherRcvQueue
|
||||
`shouldReturn` Left (SEBadConnType CDuplex)
|
||||
|
||||
testSetRcvQueueStatus :: SpecWith SQLiteStore
|
||||
testSetRcvQueueStatus =
|
||||
it "should update status of RcvQueue" $ \store -> do
|
||||
it "should update status of RcvQueue" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
setRcvQueueStatus store rcvQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1 {status = Confirmed})
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rcvQueue1))
|
||||
setRcvQueueStatus db rcvQueue1 Confirmed
|
||||
`shouldReturn` ()
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rcvQueue1 {status = Confirmed}))
|
||||
|
||||
testSetSndQueueStatus :: SpecWith SQLiteStore
|
||||
testSetSndQueueStatus =
|
||||
it "should update status of SndQueue" $ \store -> do
|
||||
it "should update status of SndQueue" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1 {status = Confirmed})
|
||||
_ <- createSndConn db g cData1 sndQueue1
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sndQueue1))
|
||||
setSndQueueStatus db sndQueue1 Confirmed
|
||||
`shouldReturn` ()
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sndQueue1 {status = Confirmed}))
|
||||
|
||||
testSetQueueStatusDuplex :: SpecWith SQLiteStore
|
||||
testSetQueueStatusDuplex =
|
||||
it "should update statuses of RcvQueue and SndQueue in DuplexConnection" $ \store -> do
|
||||
it "should update statuses of RcvQueue and SndQueue in DuplexConnection" . withStoreTransaction $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
setRcvQueueStatus store rcvQueue1 Secured
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1)
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed})
|
||||
_ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- upgradeRcvConnToDuplex db "conn1" sndQueue1
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1))
|
||||
setRcvQueueStatus db rcvQueue1 Secured
|
||||
`shouldReturn` ()
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1))
|
||||
setSndQueueStatus db sndQueue1 Confirmed
|
||||
`shouldReturn` ()
|
||||
getConn db "conn1"
|
||||
`shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed}))
|
||||
|
||||
hw :: ByteString
|
||||
hw = encodeUtf8 "Hello world!"
|
||||
@@ -398,27 +395,30 @@ mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
|
||||
broker = (brokerId, ts)
|
||||
},
|
||||
msgType = AM_A_MSG_,
|
||||
msgFlags = SMP.noMsgFlags,
|
||||
msgBody = hw,
|
||||
internalHash,
|
||||
externalPrevSndHash = "hash_from_sender"
|
||||
}
|
||||
|
||||
testCreateRcvMsg_ :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation
|
||||
testCreateRcvMsg_ st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do
|
||||
testCreateRcvMsg_ :: DB.Connection -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation
|
||||
testCreateRcvMsg_ db expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do
|
||||
let MsgMeta {recipient = (internalId, _)} = msgMeta
|
||||
updateRcvIds st connId
|
||||
`returnsResult` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)
|
||||
createRcvMsg st connId rcvMsgData
|
||||
`returnsResult` ()
|
||||
updateRcvIds db connId
|
||||
`shouldReturn` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)
|
||||
createRcvMsg db connId rcvMsgData
|
||||
`shouldReturn` ()
|
||||
|
||||
testCreateRcvMsg :: SpecWith SQLiteStore
|
||||
testCreateRcvMsg =
|
||||
it "should reserve internal ids and create a RcvMsg" $ \st -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
let ConnData {connId} = cData1
|
||||
_ <- runExceptT $ createRcvConn st g cData1 rcvQueue1 SCMInvitation
|
||||
testCreateRcvMsg_ st 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy"
|
||||
testCreateRcvMsg_ st 1 "hash_dummy" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy"
|
||||
_ <- withTransaction st $ \db -> do
|
||||
createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
withTransaction st $ \db -> do
|
||||
testCreateRcvMsg_ db 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy"
|
||||
testCreateRcvMsg_ db 1 "hash_dummy" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy"
|
||||
|
||||
mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData
|
||||
mkSndMsgData internalId internalSndId internalHash =
|
||||
@@ -427,37 +427,42 @@ mkSndMsgData internalId internalSndId internalHash =
|
||||
internalSndId,
|
||||
internalTs = ts,
|
||||
msgType = AM_A_MSG_,
|
||||
msgFlags = SMP.noMsgFlags,
|
||||
msgBody = hw,
|
||||
internalHash,
|
||||
prevMsgHash = internalHash
|
||||
}
|
||||
|
||||
testCreateSndMsg_ :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation
|
||||
testCreateSndMsg_ store expectedPrevHash connId sndMsgData@SndMsgData {..} = do
|
||||
updateSndIds store connId
|
||||
`returnsResult` (internalId, internalSndId, expectedPrevHash)
|
||||
createSndMsg store connId sndMsgData
|
||||
`returnsResult` ()
|
||||
testCreateSndMsg_ :: DB.Connection -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation
|
||||
testCreateSndMsg_ db expectedPrevHash connId sndMsgData@SndMsgData {..} = do
|
||||
updateSndIds db connId
|
||||
`shouldReturn` (internalId, internalSndId, expectedPrevHash)
|
||||
createSndMsg db connId sndMsgData
|
||||
`shouldReturn` ()
|
||||
|
||||
testCreateSndMsg :: SpecWith SQLiteStore
|
||||
testCreateSndMsg =
|
||||
it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \store -> do
|
||||
it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \st -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
let ConnData {connId} = cData1
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
testCreateSndMsg_ store "" connId $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy"
|
||||
testCreateSndMsg_ store "hash_dummy" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy"
|
||||
_ <- withTransaction st $ \db -> do
|
||||
createSndConn db g cData1 sndQueue1
|
||||
withTransaction st $ \db -> do
|
||||
testCreateSndMsg_ db "" connId $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy"
|
||||
testCreateSndMsg_ db "hash_dummy" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy"
|
||||
|
||||
testCreateRcvAndSndMsgs :: SpecWith SQLiteStore
|
||||
testCreateRcvAndSndMsgs =
|
||||
it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \st -> do
|
||||
let ConnData {connId} = cData1
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
testCreateRcvMsg_ store 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1"
|
||||
testCreateRcvMsg_ store 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2"
|
||||
testCreateSndMsg_ store "" connId $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1"
|
||||
testCreateRcvMsg_ store 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
|
||||
testCreateSndMsg_ store "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
|
||||
testCreateSndMsg_ store "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
|
||||
_ <- withTransaction st $ \db -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
withTransaction st $ \db -> do
|
||||
_ <- upgradeRcvConnToDuplex db "conn1" sndQueue1
|
||||
testCreateRcvMsg_ db 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1"
|
||||
testCreateRcvMsg_ db 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2"
|
||||
testCreateSndMsg_ db "" connId $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1"
|
||||
testCreateRcvMsg_ db 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
|
||||
testCreateSndMsg_ db "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
|
||||
testCreateSndMsg_ db "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
|
||||
|
||||
@@ -20,7 +20,7 @@ schemaDumpTest =
|
||||
|
||||
testVerifySchemaDump :: IO ()
|
||||
testVerifySchemaDump = do
|
||||
void $ createSQLiteStore testDB 1 Migrations.app False
|
||||
void $ createSQLiteStore testDB Migrations.app False
|
||||
void $ readCreateProcess (shell $ "touch " <> schema) ""
|
||||
savedSchema <- readFile schema
|
||||
savedSchema `seq` pure ()
|
||||
|
||||
+35
-10
@@ -18,8 +18,9 @@ import Control.Monad
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Types as JT
|
||||
import Data.ByteString.Builder (lazyByteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Text (Text)
|
||||
@@ -42,6 +43,7 @@ import Simplex.Messaging.Transport.HTTP2 (http2TLSParams)
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
import Simplex.Messaging.Transport.HTTP2.Server
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Test.Hspec
|
||||
import UnliftIO.Async
|
||||
import UnliftIO.Concurrent
|
||||
import qualified UnliftIO.Exception as E
|
||||
@@ -60,10 +62,13 @@ apnsTestPort = "6010"
|
||||
testKeyHash :: C.KeyHash
|
||||
testKeyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI="
|
||||
|
||||
ntfTestStoreLogFile :: FilePath
|
||||
ntfTestStoreLogFile = "tests/tmp/ntf-server-store.log"
|
||||
|
||||
testNtfClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a
|
||||
testNtfClient client =
|
||||
runTransportClient testHost ntfTestPort (Just testKeyHash) (Just defaultKeepAliveOpts) $ \h ->
|
||||
liftIO (runExceptT $ ntfClientHandshake h testKeyHash) >>= \case
|
||||
liftIO (runExceptT $ ntfClientHandshake h testKeyHash supportedNTFServerVRange) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
|
||||
@@ -79,21 +84,28 @@ ntfServerCfg =
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
apnsConfig =
|
||||
defaultAPNSPushClientConfig
|
||||
{ apnsHost = "localhost",
|
||||
apnsPort = apnsTestPort,
|
||||
{ apnsPort = apnsTestPort,
|
||||
http2cfg = defaultHTTP2ClientConfig {caStoreFile = "tests/fixtures/ca.crt"}
|
||||
},
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
storeLogFile = Nothing,
|
||||
resubscribeDelay = 1000,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
}
|
||||
|
||||
withNtfServerStoreLog :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ThreadId -> m a) -> m a
|
||||
withNtfServerStoreLog t = withNtfServerCfg t ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile}
|
||||
|
||||
withNtfServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
|
||||
withNtfServerThreadOn t port' =
|
||||
withNtfServerThreadOn t port' = withNtfServerCfg t ntfServerCfg {transports = [(port', t)]}
|
||||
|
||||
withNtfServerCfg :: (MonadUnliftIO m, MonadRandom m) => ATransport -> NtfServerConfig -> (ThreadId -> m a) -> m a
|
||||
withNtfServerCfg t cfg =
|
||||
serverBracket
|
||||
(\started -> runNtfServerBlocking started ntfServerCfg {transports = [(port', t)]})
|
||||
(\started -> runNtfServerBlocking started cfg {transports = [(ntfTestPort, t)]})
|
||||
(pure ())
|
||||
|
||||
serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a
|
||||
@@ -134,6 +146,9 @@ 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 -> (THandle c -> IO ()) -> Expectation
|
||||
ntfTest _ test' = runNtfTest test' `shouldReturn` ()
|
||||
|
||||
data APNSMockRequest = APNSMockRequest
|
||||
{ notification :: APNSNotification,
|
||||
sendApnsResponse :: APNSMockResponse -> IO ()
|
||||
@@ -163,9 +178,16 @@ withAPNSMockServer = E.bracket (getAPNSMockServer apnsMockServerConfig) closeAPN
|
||||
|
||||
deriving instance Generic APNSAlertBody
|
||||
|
||||
deriving instance FromJSON APNSAlertBody
|
||||
instance FromJSON APNSAlertBody where
|
||||
parseJSON (J.Object v) = do
|
||||
title <- v .: "title"
|
||||
subtitle <- v .: "subtitle"
|
||||
body <- v .: "body"
|
||||
pure APNSAlertObject {title, subtitle, body}
|
||||
parseJSON (J.String v) = pure $ APNSAlertText v
|
||||
parseJSON invalid = JT.prependFailure "parsing Coord failed, " (JT.typeMismatch "Object" invalid)
|
||||
|
||||
instance FromJSON APNSNotificationBody where parseJSON = J.genericParseJSON apnsJSONOptions
|
||||
instance FromJSON APNSNotificationBody where parseJSON = J.genericParseJSON apnsJSONOptions {J.rejectUnknownFields = True}
|
||||
|
||||
deriving instance FromJSON APNSNotification
|
||||
|
||||
@@ -185,8 +207,11 @@ getAPNSMockServer config@HTTP2ServerConfig {qSize} = do
|
||||
APNSRespError status reason ->
|
||||
sendResponse . H.responseBuilder status [] . lazyByteString $ J.encode APNSErrorResponse {reason}
|
||||
case J.decodeStrict' reqBody of
|
||||
Just notification -> atomically $ writeTBQueue apnsQ APNSMockRequest {notification, sendApnsResponse}
|
||||
_ -> sendApnsResponse $ APNSRespError N.badRequest400 "bad_request_body"
|
||||
Just notification ->
|
||||
atomically $ writeTBQueue apnsQ APNSMockRequest {notification, sendApnsResponse}
|
||||
_ -> do
|
||||
putStrLn $ "runAPNSMockServer J.decodeStrict' error, reqBody: " <> show reqBody
|
||||
sendApnsResponse $ APNSRespError N.badRequest400 "bad_request_body"
|
||||
|
||||
closeAPNSMockServer :: APNSMockServer -> IO ()
|
||||
closeAPNSMockServer APNSMockServer {action, http2Server} = do
|
||||
|
||||
+134
-6
@@ -1,32 +1,59 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module NtfServerTests where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Types as JT
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import NtfClient
|
||||
import ServerTests (sampleDhPubKey, samplePubKey, sampleSig)
|
||||
import SMPClient as SMP
|
||||
import ServerTests
|
||||
( createAndSecureQueue,
|
||||
sampleDhPubKey,
|
||||
samplePubKey,
|
||||
sampleSig,
|
||||
signSendRecv,
|
||||
(#==),
|
||||
_SEND',
|
||||
pattern Resp,
|
||||
)
|
||||
import qualified Simplex.Messaging.Agent.Protocol as AP
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import qualified Simplex.Messaging.Notifications.Server.Push.APNS as APNS
|
||||
import Simplex.Messaging.Parsers (parse)
|
||||
import Simplex.Messaging.Protocol hiding (notification)
|
||||
import Simplex.Messaging.Transport
|
||||
import Test.Hspec
|
||||
import UnliftIO.STM
|
||||
|
||||
ntfServerTests :: ATransport -> Spec
|
||||
ntfServerTests t = do
|
||||
describe "notifications server protocol syntax" $ ntfSyntaxTests t
|
||||
describe "Notifications server protocol syntax" $ ntfSyntaxTests t
|
||||
describe "Notification subscriptions" $ testNotificationSubscription t
|
||||
|
||||
ntfSyntaxTests :: ATransport -> Spec
|
||||
ntfSyntaxTests (ATransport t) = do
|
||||
it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN)
|
||||
describe "NEW" $ do
|
||||
it "no parameters" $ (sampleSig, "bcda", "", TNEW_) >#> ("", "bcda", "", ERR $ CMD SYNTAX)
|
||||
it "many parameters" $ (sampleSig, "cdab", "", (TNEW_, (' ', '\x01', 'A'), ('T', 'A', "abcd" :: ByteString), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX)
|
||||
it "no signature" $ ("", "dabc", "", (TNEW_, ' ', ('T', 'A', "abcd" :: ByteString), samplePubKey, sampleDhPubKey)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH)
|
||||
it "token ID" $ (sampleSig, "abcd", "12345678", (TNEW_, ' ', ('T', 'A', "abcd" :: ByteString), samplePubKey, sampleDhPubKey)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH)
|
||||
it "many parameters" $ (sampleSig, "cdab", "", (TNEW_, (' ', '\x01', 'A'), ('T', 'A', 'T', "abcd" :: ByteString), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX)
|
||||
it "no signature" $ ("", "dabc", "", (TNEW_, ' ', ('T', 'A', 'T', "abcd" :: ByteString), samplePubKey, sampleDhPubKey)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH)
|
||||
it "token ID" $ (sampleSig, "abcd", "12345678", (TNEW_, ' ', ('T', 'A', 'T', "abcd" :: ByteString), samplePubKey, sampleDhPubKey)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH)
|
||||
where
|
||||
(>#>) ::
|
||||
Encoding smp =>
|
||||
@@ -34,3 +61,104 @@ ntfSyntaxTests (ATransport t) = do
|
||||
(Maybe C.ASignature, ByteString, ByteString, BrokerMsg) ->
|
||||
Expectation
|
||||
command >#> response = withAPNSMockServer $ \_ -> ntfServerTest t command `shouldReturn` response
|
||||
|
||||
pattern RespNtf :: CorrId -> QueueId -> NtfResponse -> SignedTransmission NtfResponse
|
||||
pattern RespNtf corrId queueId command <- (_, _, (corrId, queueId, Right command))
|
||||
|
||||
sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandle c -> (Maybe C.ASignature, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission NtfResponse)
|
||||
sendRecvNtf h@THandle {thVersion, sessionId} (sgn, corrId, qId, cmd) = do
|
||||
let t = encodeTransmission thVersion sessionId (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut h (sgn, t)
|
||||
tGet h
|
||||
|
||||
signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandle c -> C.APrivateSignKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission NtfResponse)
|
||||
signSendRecvNtf h@THandle {thVersion, sessionId} pk (corrId, qId, cmd) = do
|
||||
let t = encodeTransmission thVersion sessionId (CorrId corrId, qId, cmd)
|
||||
Right sig <- runExceptT $ C.sign pk t
|
||||
Right () <- tPut h (Just sig, t)
|
||||
tGet h
|
||||
|
||||
(.->) :: J.Value -> J.Key -> Either String ByteString
|
||||
v .-> key =
|
||||
let J.Object o = v
|
||||
in U.decodeLenient . encodeUtf8 <$> JT.parseEither (J..: key) o
|
||||
|
||||
testNotificationSubscription :: ATransport -> Spec
|
||||
testNotificationSubscription (ATransport t) =
|
||||
it "should create notification subscription and notify when message is received" $ do
|
||||
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
|
||||
(nPub, nKey) <- C.generateSignatureKeyPair C.SEd25519
|
||||
(tknPub, tknKey) <- C.generateSignatureKeyPair C.SEd25519
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
withAPNSMockServer $ \APNSMockServer {apnsQ} ->
|
||||
smpTest2 t $ \rh sh ->
|
||||
ntfTest t $ \nh -> do
|
||||
-- create queue
|
||||
(sId, rId, rKey, rcvDhSecret) <- createAndSecureQueue rh sPub
|
||||
-- register and verify token
|
||||
RespNtf "1" "" (NRTknId tId ntfDh) <- signSendRecvNtf nh tknKey ("1", "", TNEW $ NewNtfTkn tkn tknPub dhPub)
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse = send} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
send APNSRespOk
|
||||
let dhSecret = C.dh' ntfDh dhPriv
|
||||
Right verification = ntfData .-> "verification"
|
||||
Right nonce = C.cbNonce <$> ntfData .-> "nonce"
|
||||
Right code = NtfRegCode <$> C.cbDecrypt dhSecret nonce verification
|
||||
RespNtf "2" _ NROk <- signSendRecvNtf nh tknKey ("2", tId, TVFY code)
|
||||
RespNtf "2a" _ (NRTkn NTActive) <- signSendRecvNtf nh tknKey ("2a", tId, TCHK)
|
||||
-- enable queue notifications
|
||||
(rcvNtfPubDhKey, rcvNtfPrivDhKey) <- C.generateKeyPair'
|
||||
Resp "3" _ (NID nId rcvNtfSrvPubDhKey) <- signSendRecv rh rKey ("3", rId, NKEY nPub rcvNtfPubDhKey)
|
||||
let srv = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
q = SMPQueueNtf srv nId
|
||||
rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
|
||||
RespNtf "4" _ (NRSubId _subId) <- signSendRecvNtf nh tknKey ("4", "", SNEW $ NewNtfSub tId q nKey)
|
||||
-- send message
|
||||
threadDelay 50000
|
||||
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, _SEND' "hello")
|
||||
-- receive notification
|
||||
APNSMockRequest {notification, sendApnsResponse = send'} <- atomically $ readTBQueue apnsQ
|
||||
let APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData'} = notification
|
||||
Right nonce' = C.cbNonce <$> ntfData' .-> "nonce"
|
||||
Right message = ntfData' .-> "message"
|
||||
Right ntfDataDecrypted = C.cbDecrypt dhSecret nonce' message
|
||||
Right APNS.PNMessageData {smpQueue = SMPQueueNtf {smpServer, notifierId}, nmsgNonce, encNMsgMeta} =
|
||||
parse strP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted
|
||||
Right nMsgMeta = C.cbDecrypt rcvNtfDhSecret nmsgNonce encNMsgMeta
|
||||
Right NMsgMeta {msgId, msgTs} = parse smpP (AP.INTERNAL "error parsing NMsgMeta") nMsgMeta
|
||||
smpServer `shouldBe` srv
|
||||
notifierId `shouldBe` nId
|
||||
send' APNSRespOk
|
||||
-- receive message
|
||||
Resp "" _ (MSG mId1 mTs _ msg1) <- tGet rh
|
||||
mId1 `shouldBe` msgId
|
||||
mTs `shouldBe` msgTs
|
||||
let decryptedMsg = C.cbDecrypt rcvDhSecret (C.cbNonce mId1) msg1
|
||||
(decryptedMsg, Right "hello") #== "delivered from queue"
|
||||
Resp "6" _ OK <- signSendRecv rh rKey ("6", rId, ACK mId1)
|
||||
pure ()
|
||||
-- replace token
|
||||
let tkn' = DeviceToken PPApnsTest "efgh"
|
||||
RespNtf "7" tId' NROk <- signSendRecvNtf nh tknKey ("7", tId, TRPL tkn')
|
||||
tId `shouldBe` tId'
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData2}, sendApnsResponse = send2} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
send2 APNSRespOk
|
||||
let Right verification2 = ntfData2 .-> "verification"
|
||||
Right nonce2 = C.cbNonce <$> ntfData2 .-> "nonce"
|
||||
Right code2 = NtfRegCode <$> C.cbDecrypt dhSecret nonce2 verification2
|
||||
RespNtf "8" _ NROk <- signSendRecvNtf nh tknKey ("8", tId, TVFY code2)
|
||||
RespNtf "8a" _ (NRTkn NTActive) <- signSendRecvNtf nh tknKey ("8a", tId, TCHK)
|
||||
-- send message
|
||||
Resp "9" _ OK <- signSendRecv sh sKey ("9", sId, _SEND' "hello 2")
|
||||
APNSMockRequest {notification = notification3, sendApnsResponse = send3} <- atomically $ readTBQueue apnsQ
|
||||
let APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData3} = notification3
|
||||
Right nonce3 = C.cbNonce <$> ntfData3 .-> "nonce"
|
||||
Right message3 = ntfData3 .-> "message"
|
||||
Right ntfDataDecrypted3 = C.cbDecrypt dhSecret nonce3 message3
|
||||
Right APNS.PNMessageData {smpQueue = SMPQueueNtf {smpServer = smpServer3, notifierId = notifierId3}} =
|
||||
parse strP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted3
|
||||
smpServer3 `shouldBe` srv
|
||||
notifierId3 `shouldBe` nId
|
||||
send3 APNSRespOk
|
||||
|
||||
@@ -183,6 +183,7 @@ agentCfg =
|
||||
defaultTransport = (ntfTestPort, transport @TLS)
|
||||
},
|
||||
reconnectInterval = defaultReconnectInterval {initialInterval = 50_000},
|
||||
ntfWorkerThrottle = 1000,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
|
||||
+3
-2
@@ -49,7 +49,7 @@ testStoreMsgsFile = "tests/tmp/smp-server-messages.log"
|
||||
testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a
|
||||
testSMPClient client =
|
||||
runTransportClient testHost testPort (Just testKeyHash) (Just defaultKeepAliveOpts) $ \h ->
|
||||
liftIO (runExceptT $ smpClientHandshake h testKeyHash) >>= \case
|
||||
liftIO (runExceptT $ smpClientHandshake h testKeyHash supportedSMPServerVRange) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
|
||||
@@ -71,7 +71,8 @@ cfg =
|
||||
logStatsStartTime = 0,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
smpServerVRange = supportedSMPServerVRange
|
||||
}
|
||||
|
||||
withSmpServerStoreMsgLogOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
|
||||
|
||||
+181
-90
@@ -41,6 +41,8 @@ serverTests t@(ATransport t') = do
|
||||
describe "SMP messages" $ do
|
||||
describe "duplex communication over 2 SMP connections" $ testDuplex t
|
||||
describe "switch subscription to another TCP connection" $ testSwitchSub t
|
||||
describe "GET command" $ testGetCommand t'
|
||||
describe "GET & SUB commands" $ testGetSubCommands t'
|
||||
describe "Store log" $ testWithStoreLog t
|
||||
describe "Restore messages" $ testRestoreMessages t
|
||||
describe "Timing of AUTH error" $ testTiming t
|
||||
@@ -57,14 +59,14 @@ pattern Ids :: RecipientId -> SenderId -> RcvPublicDhKey -> BrokerMsg
|
||||
pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh)
|
||||
|
||||
sendRecv :: forall c p. (Transport c, PartyI p) => THandle c -> (Maybe C.ASignature, ByteString, ByteString, Command p) -> IO (SignedTransmission BrokerMsg)
|
||||
sendRecv h@THandle {sessionId} (sgn, corrId, qId, cmd) = do
|
||||
let t = encodeTransmission sessionId (CorrId corrId, qId, cmd)
|
||||
sendRecv h@THandle {thVersion, sessionId} (sgn, corrId, qId, cmd) = do
|
||||
let t = encodeTransmission thVersion sessionId (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut h (sgn, t)
|
||||
tGet h
|
||||
|
||||
signSendRecv :: forall c p. (Transport c, PartyI p) => THandle c -> C.APrivateSignKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission BrokerMsg)
|
||||
signSendRecv h@THandle {sessionId} pk (corrId, qId, cmd) = do
|
||||
let t = encodeTransmission sessionId (CorrId corrId, qId, cmd)
|
||||
signSendRecv h@THandle {thVersion, sessionId} pk (corrId, qId, cmd) = do
|
||||
let t = encodeTransmission thVersion sessionId (CorrId corrId, qId, cmd)
|
||||
Right sig <- runExceptT $ C.sign pk t
|
||||
Right () <- tPut h (Just sig, t)
|
||||
tGet h
|
||||
@@ -72,6 +74,12 @@ signSendRecv h@THandle {sessionId} pk (corrId, qId, cmd) = do
|
||||
(#==) :: (HasCallStack, Eq a, Show a) => (a, a) -> String -> Assertion
|
||||
(actual, expected) #== message = assertEqual message expected actual
|
||||
|
||||
_SEND :: MsgBody -> Command 'Sender
|
||||
_SEND = SEND noMsgFlags
|
||||
|
||||
_SEND' :: MsgBody -> Command 'Sender
|
||||
_SEND' = SEND MsgFlags {notification = True}
|
||||
|
||||
testCreateSecure :: ATransport -> Spec
|
||||
testCreateSecure (ATransport t) =
|
||||
it "should create (NEW) and secure (KEY) queue" $
|
||||
@@ -82,21 +90,21 @@ testCreateSecure (ATransport t) =
|
||||
let dec nonce = C.cbDecrypt (C.dh' srvDh dhPriv) (C.cbNonce nonce)
|
||||
(rId1, "") #== "creates queue"
|
||||
|
||||
Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, SEND "hello")
|
||||
Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, _SEND "hello")
|
||||
(ok1, OK) #== "accepts unsigned SEND"
|
||||
(sId1, sId) #== "same queue ID in response 1"
|
||||
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet h
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet h
|
||||
(dec mId1 msg1, Right "hello") #== "delivers message"
|
||||
|
||||
Resp "cdab" _ ok4 <- signSendRecv h rKey ("cdab", rId, ACK)
|
||||
Resp "cdab" _ ok4 <- signSendRecv h rKey ("cdab", rId, ACK mId1)
|
||||
(ok4, OK) #== "replies OK when message acknowledged if no more messages"
|
||||
|
||||
Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, ACK)
|
||||
Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, ACK mId1)
|
||||
(err6, ERR NO_MSG) #== "replies ERR when message acknowledged without messages"
|
||||
|
||||
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd448
|
||||
Resp "abcd" sId2 err1 <- signSendRecv h sKey ("abcd", sId, SEND "hello")
|
||||
Resp "abcd" sId2 err1 <- signSendRecv h sKey ("abcd", sId, _SEND "hello")
|
||||
(err1, ERR AUTH) #== "rejects signed SEND"
|
||||
(sId2, sId) #== "same queue ID in response 2"
|
||||
|
||||
@@ -113,16 +121,16 @@ testCreateSecure (ATransport t) =
|
||||
Resp "abcd" _ err4 <- signSendRecv h rKey ("abcd", rId, KEY sPub)
|
||||
(err4, ERR AUTH) #== "rejects KEY if already secured"
|
||||
|
||||
Resp "bcda" _ ok3 <- signSendRecv h sKey ("bcda", sId, SEND "hello again")
|
||||
Resp "bcda" _ ok3 <- signSendRecv h sKey ("bcda", sId, _SEND "hello again")
|
||||
(ok3, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp "" _ (MSG mId2 _ msg2) <- tGet h
|
||||
Resp "" _ (MSG mId2 _ _ msg2) <- tGet h
|
||||
(dec mId2 msg2, Right "hello again") #== "delivers message 2"
|
||||
|
||||
Resp "cdab" _ ok5 <- signSendRecv h rKey ("cdab", rId, ACK)
|
||||
Resp "cdab" _ ok5 <- signSendRecv h rKey ("cdab", rId, ACK mId2)
|
||||
(ok5, OK) #== "replies OK when message acknowledged 2"
|
||||
|
||||
Resp "dabc" _ err5 <- sendRecv h ("", "dabc", sId, SEND "hello")
|
||||
Resp "dabc" _ err5 <- sendRecv h ("", "dabc", sId, _SEND "hello")
|
||||
(err5, ERR AUTH) #== "rejects unsigned SEND"
|
||||
|
||||
testCreateDelete :: ATransport -> Spec
|
||||
@@ -139,13 +147,13 @@ testCreateDelete (ATransport t) =
|
||||
Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, KEY sPub)
|
||||
(ok1, OK) #== "secures queue"
|
||||
|
||||
Resp "cdab" _ ok2 <- signSendRecv sh sKey ("cdab", sId, SEND "hello")
|
||||
Resp "cdab" _ ok2 <- signSendRecv sh sKey ("cdab", sId, _SEND "hello")
|
||||
(ok2, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp "dabc" _ ok7 <- signSendRecv sh sKey ("dabc", sId, SEND "hello 2")
|
||||
Resp "dabc" _ ok7 <- signSendRecv sh sKey ("dabc", sId, _SEND "hello 2")
|
||||
(ok7, OK) #== "accepts signed SEND 2 - this message is not delivered because the first is not ACKed"
|
||||
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet rh
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet rh
|
||||
(dec mId1 msg1, Right "hello") #== "delivers message"
|
||||
|
||||
Resp "abcd" _ err1 <- sendRecv rh (sampleSig, "abcd", rId, OFF)
|
||||
@@ -158,16 +166,16 @@ testCreateDelete (ATransport t) =
|
||||
(ok3, OK) #== "suspends queue"
|
||||
(rId2, rId) #== "same queue ID in response 2"
|
||||
|
||||
Resp "dabc" _ err3 <- signSendRecv sh sKey ("dabc", sId, SEND "hello")
|
||||
Resp "dabc" _ err3 <- signSendRecv sh sKey ("dabc", sId, _SEND "hello")
|
||||
(err3, ERR AUTH) #== "rejects signed SEND"
|
||||
|
||||
Resp "abcd" _ err4 <- sendRecv sh ("", "abcd", sId, SEND "hello")
|
||||
Resp "abcd" _ err4 <- sendRecv sh ("", "abcd", sId, _SEND "hello")
|
||||
(err4, ERR AUTH) #== "reject unsigned SEND too"
|
||||
|
||||
Resp "bcda" _ ok4 <- signSendRecv rh rKey ("bcda", rId, OFF)
|
||||
(ok4, OK) #== "accepts OFF when suspended"
|
||||
|
||||
Resp "cdab" _ (MSG mId2 _ msg2) <- signSendRecv rh rKey ("cdab", rId, SUB)
|
||||
Resp "cdab" _ (MSG mId2 _ _ msg2) <- signSendRecv rh rKey ("cdab", rId, SUB)
|
||||
(dec mId2 msg2, Right "hello") #== "accepts SUB when suspended and delivers the message again (because was not ACKed)"
|
||||
|
||||
Resp "dabc" _ err5 <- sendRecv rh (sampleSig, "dabc", rId, DEL)
|
||||
@@ -180,13 +188,13 @@ testCreateDelete (ATransport t) =
|
||||
(ok6, OK) #== "deletes queue"
|
||||
(rId3, rId) #== "same queue ID in response 3"
|
||||
|
||||
Resp "cdab" _ err7 <- signSendRecv sh sKey ("cdab", sId, SEND "hello")
|
||||
Resp "cdab" _ err7 <- signSendRecv sh sKey ("cdab", sId, _SEND "hello")
|
||||
(err7, ERR AUTH) #== "rejects signed SEND when deleted"
|
||||
|
||||
Resp "dabc" _ err8 <- sendRecv sh ("", "dabc", sId, SEND "hello")
|
||||
Resp "dabc" _ err8 <- sendRecv sh ("", "dabc", sId, _SEND "hello")
|
||||
(err8, ERR AUTH) #== "rejects unsigned SEND too when deleted"
|
||||
|
||||
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, ACK)
|
||||
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, ACK "")
|
||||
(err11, ERR AUTH) #== "rejects ACK when conn deleted - the second message is deleted"
|
||||
|
||||
Resp "bcda" _ err9 <- signSendRecv rh rKey ("bcda", rId, OFF)
|
||||
@@ -233,11 +241,11 @@ testDuplex (ATransport t) =
|
||||
-- aSnd ID is passed to Bob out-of-band
|
||||
|
||||
(bsPub, bsKey) <- C.generateSignatureKeyPair C.SEd448
|
||||
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, SEND $ "key " <> strEncode bsPub)
|
||||
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, _SEND $ "key " <> strEncode bsPub)
|
||||
-- "key ..." is ad-hoc, not a part of SMP protocol
|
||||
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK)
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK mId1)
|
||||
Right ["key", bobKey] <- pure $ B.words <$> aDec mId1 msg1
|
||||
(bobKey, strEncode bsPub) #== "key received from Bob"
|
||||
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, KEY bsPub)
|
||||
@@ -246,34 +254,34 @@ testDuplex (ATransport t) =
|
||||
(bDhPub, bDhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
|
||||
Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub)
|
||||
let bDec nonce = C.cbDecrypt (C.dh' bSrvDh bDhPriv) (C.cbNonce nonce)
|
||||
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, SEND $ "reply_id " <> encode bSnd)
|
||||
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, _SEND $ "reply_id " <> encode bSnd)
|
||||
-- "reply_id ..." is ad-hoc, not a part of SMP protocol
|
||||
|
||||
Resp "" _ (MSG mId2 _ msg2) <- tGet alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK)
|
||||
Resp "" _ (MSG mId2 _ _ msg2) <- tGet alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK mId2)
|
||||
Right ["reply_id", bId] <- pure $ B.words <$> aDec mId2 msg2
|
||||
(bId, encode bSnd) #== "reply queue ID received from Bob"
|
||||
|
||||
(asPub, asKey) <- C.generateSignatureKeyPair C.SEd448
|
||||
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, SEND $ "key " <> strEncode asPub)
|
||||
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, _SEND $ "key " <> strEncode asPub)
|
||||
-- "key ..." is ad-hoc, not a part of SMP protocol
|
||||
|
||||
Resp "" _ (MSG mId3 _ msg3) <- tGet bob
|
||||
Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, ACK)
|
||||
Resp "" _ (MSG mId3 _ _ msg3) <- tGet bob
|
||||
Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, ACK mId3)
|
||||
Right ["key", aliceKey] <- pure $ B.words <$> bDec mId3 msg3
|
||||
(aliceKey, strEncode asPub) #== "key received from Alice"
|
||||
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, KEY asPub)
|
||||
|
||||
Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, SEND "hi alice")
|
||||
Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, _SEND "hi alice")
|
||||
|
||||
Resp "" _ (MSG mId4 _ msg4) <- tGet alice
|
||||
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, ACK)
|
||||
Resp "" _ (MSG mId4 _ _ msg4) <- tGet alice
|
||||
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, ACK mId4)
|
||||
(aDec mId4 msg4, Right "hi alice") #== "message received from Bob"
|
||||
|
||||
Resp "abcd" _ OK <- signSendRecv alice asKey ("abcd", bSnd, SEND "how are you bob")
|
||||
Resp "abcd" _ OK <- signSendRecv alice asKey ("abcd", bSnd, _SEND "how are you bob")
|
||||
|
||||
Resp "" _ (MSG mId5 _ msg5) <- tGet bob
|
||||
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, ACK)
|
||||
Resp "" _ (MSG mId5 _ _ msg5) <- tGet bob
|
||||
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, ACK mId5)
|
||||
(bDec mId5 msg5, Right "how are you bob") #== "message received from alice"
|
||||
|
||||
testSwitchSub :: ATransport -> Spec
|
||||
@@ -284,38 +292,105 @@ testSwitchSub (ATransport t) =
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
|
||||
Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub)
|
||||
let dec nonce = C.cbDecrypt (C.dh' srvDh dhPriv) (C.cbNonce nonce)
|
||||
Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, SEND "test1")
|
||||
Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, _SEND "test1")
|
||||
(ok1, OK) #== "sent test message 1"
|
||||
Resp "cdab" _ ok2 <- sendRecv sh ("", "cdab", sId, SEND "test2, no ACK")
|
||||
Resp "cdab" _ ok2 <- sendRecv sh ("", "cdab", sId, _SEND "test2, no ACK")
|
||||
(ok2, OK) #== "sent test message 2"
|
||||
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet rh1
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet rh1
|
||||
(dec mId1 msg1, Right "test1") #== "test message 1 delivered to the 1st TCP connection"
|
||||
Resp "abcd" _ (MSG mId2 _ msg2) <- signSendRecv rh1 rKey ("abcd", rId, ACK)
|
||||
Resp "abcd" _ (MSG mId2 _ _ msg2) <- signSendRecv rh1 rKey ("abcd", rId, ACK mId1)
|
||||
(dec mId2 msg2, Right "test2, no ACK") #== "test message 2 delivered, no ACK"
|
||||
|
||||
Resp "bcda" _ (MSG mId2' _ msg2') <- signSendRecv rh2 rKey ("bcda", rId, SUB)
|
||||
Resp "bcda" _ (MSG mId2' _ _ msg2') <- signSendRecv rh2 rKey ("bcda", rId, SUB)
|
||||
(dec mId2' msg2', Right "test2, no ACK") #== "same simplex queue via another TCP connection, tes2 delivered again (no ACK in 1st queue)"
|
||||
Resp "cdab" _ OK <- signSendRecv rh2 rKey ("cdab", rId, ACK)
|
||||
Resp "cdab" _ OK <- signSendRecv rh2 rKey ("cdab", rId, ACK mId2')
|
||||
|
||||
Resp "" _ end <- tGet rh1
|
||||
(end, END) #== "unsubscribed the 1st TCP connection"
|
||||
|
||||
Resp "dabc" _ OK <- sendRecv sh ("", "dabc", sId, SEND "test3")
|
||||
Resp "dabc" _ OK <- sendRecv sh ("", "dabc", sId, _SEND "test3")
|
||||
|
||||
Resp "" _ (MSG mId3 _ msg3) <- tGet rh2
|
||||
Resp "" _ (MSG mId3 _ _ msg3) <- tGet rh2
|
||||
(dec mId3 msg3, Right "test3") #== "delivered to the 2nd TCP connection"
|
||||
|
||||
Resp "abcd" _ err <- signSendRecv rh1 rKey ("abcd", rId, ACK)
|
||||
Resp "abcd" _ err <- signSendRecv rh1 rKey ("abcd", rId, ACK mId3)
|
||||
(err, ERR NO_MSG) #== "rejects ACK from the 1st TCP connection"
|
||||
|
||||
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, ACK)
|
||||
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, ACK mId3)
|
||||
(ok3, OK) #== "accepts ACK from the 2nd TCP connection"
|
||||
|
||||
1000 `timeout` tGet @BrokerMsg rh1 >>= \case
|
||||
Nothing -> return ()
|
||||
Just _ -> error "nothing else is delivered to the 1st TCP connection"
|
||||
|
||||
testGetCommand :: forall c. Transport c => TProxy c -> Spec
|
||||
testGetCommand t =
|
||||
it "should retrieve messages from the queue using GET command" $ do
|
||||
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
|
||||
smpTest t $ \sh -> do
|
||||
queue <- newEmptyTMVarIO
|
||||
testSMPClient @c $ \rh ->
|
||||
atomically . putTMVar queue =<< createAndSecureQueue rh sPub
|
||||
testSMPClient @c $ \rh -> do
|
||||
(sId, rId, rKey, dhShared) <- atomically $ takeTMVar queue
|
||||
let dec nonce = C.cbDecrypt dhShared (C.cbNonce nonce)
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello")
|
||||
Resp "2" _ (MSG mId1 _ _ msg1) <- signSendRecv rh rKey ("2", rId, GET)
|
||||
(dec mId1 msg1, Right "hello") #== "retrieved from queue"
|
||||
Resp "3" _ OK <- signSendRecv rh rKey ("3", rId, ACK mId1)
|
||||
Resp "4" _ OK <- signSendRecv rh rKey ("4", rId, GET)
|
||||
pure ()
|
||||
|
||||
testGetSubCommands :: forall c. Transport c => TProxy c -> Spec
|
||||
testGetSubCommands t =
|
||||
it "should retrieve messages with GET and receive with SUB, only one ACK would work" $ do
|
||||
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
|
||||
smpTest3 t $ \rh1 rh2 sh -> do
|
||||
(sId, rId, rKey, dhShared) <- createAndSecureQueue rh1 sPub
|
||||
let dec nonce = C.cbDecrypt dhShared (C.cbNonce nonce)
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello 1")
|
||||
Resp "1a" _ OK <- signSendRecv sh sKey ("1a", sId, _SEND "hello 2")
|
||||
Resp "1b" _ OK <- signSendRecv sh sKey ("1b", sId, _SEND "hello 3")
|
||||
Resp "1c" _ OK <- signSendRecv sh sKey ("1c", sId, _SEND "hello 4")
|
||||
-- both get the same if not ACK'd
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet rh1
|
||||
Resp "2" _ (MSG mId1' _ _ msg1') <- signSendRecv rh2 rKey ("2", rId, GET)
|
||||
(dec mId1 msg1, Right "hello 1") #== "received from queue via SUB"
|
||||
(dec mId1' msg1', Right "hello 1") #== "retrieved from queue with GET"
|
||||
mId1 `shouldBe` mId1'
|
||||
msg1 `shouldBe` msg1'
|
||||
-- subscriber cannot GET, getter cannot SUB
|
||||
Resp "3" _ (ERR (CMD PROHIBITED)) <- signSendRecv rh1 rKey ("3", rId, GET)
|
||||
Resp "3a" _ (ERR (CMD PROHIBITED)) <- signSendRecv rh2 rKey ("3a", rId, SUB)
|
||||
-- ACK for SUB delivers the next message
|
||||
Resp "4" _ (MSG mId2 _ _ msg2) <- signSendRecv rh1 rKey ("4", rId, ACK mId1)
|
||||
(dec mId2 msg2, Right "hello 2") #== "received from queue via SUB"
|
||||
-- bad msgId returns error
|
||||
Resp "5" _ (ERR NO_MSG) <- signSendRecv rh2 rKey ("5", rId, ACK "1234")
|
||||
-- already ACK'd by subscriber, but still returns OK when msgId matches
|
||||
Resp "5a" _ OK <- signSendRecv rh2 rKey ("5a", rId, ACK mId1)
|
||||
-- msg2 is not lost - even if subscriber does not ACK it, it is delivered to getter
|
||||
Resp "6" _ (MSG mId2' _ _ msg2') <- signSendRecv rh2 rKey ("6", rId, GET)
|
||||
(dec mId2' msg2', Right "hello 2") #== "retrieved from queue with GET"
|
||||
mId2 `shouldBe` mId2'
|
||||
msg2 `shouldBe` msg2'
|
||||
-- getter ACK returns OK, even though there is the next message
|
||||
Resp "7" _ OK <- signSendRecv rh2 rKey ("7", rId, ACK mId2')
|
||||
Resp "8" _ (MSG mId3 _ _ msg3) <- signSendRecv rh2 rKey ("8", rId, GET)
|
||||
(dec mId3 msg3, Right "hello 3") #== "retrieved from queue with GET"
|
||||
-- subscriber ACK does not lose message
|
||||
Resp "9" _ (MSG mId3' _ _ msg3') <- signSendRecv rh1 rKey ("9", rId, ACK mId2')
|
||||
(dec mId3' msg3', Right "hello 3") #== "retrieved from queue with GET"
|
||||
mId3 `shouldBe` mId3'
|
||||
msg3 `shouldBe` msg3'
|
||||
Resp "10" _ (MSG mId4 _ _ msg4) <- signSendRecv rh1 rKey ("10", rId, ACK mId3)
|
||||
(dec mId4 msg4, Right "hello 4") #== "retrieved from queue with GET"
|
||||
Resp "11" _ OK <- signSendRecv rh1 rKey ("11", rId, ACK mId4)
|
||||
-- no more messages for getter too
|
||||
Resp "12" _ OK <- signSendRecv rh2 rKey ("12", rId, GET)
|
||||
pure ()
|
||||
|
||||
testWithStoreLog :: ATransport -> Spec
|
||||
testWithStoreLog at@(ATransport t) =
|
||||
it "should store simplex queues to log and restore them after server restart" $ do
|
||||
@@ -331,7 +406,8 @@ testWithStoreLog at@(ATransport t) =
|
||||
|
||||
withSmpServerStoreLogOn at testPort . runTest t $ \h -> runClient t $ \h1 -> do
|
||||
(sId1, rId1, rKey1, dhShared) <- createAndSecureQueue h sPub1
|
||||
Resp "abcd" _ (NID nId) <- signSendRecv h rKey1 ("abcd", rId1, NKEY nPub)
|
||||
(rcvNtfPubDhKey, _) <- C.generateKeyPair'
|
||||
Resp "abcd" _ (NID nId _) <- signSendRecv h rKey1 ("abcd", rId1, NKEY nPub rcvNtfPubDhKey)
|
||||
atomically $ do
|
||||
writeTVar recipientId1 rId1
|
||||
writeTVar recipientKey1 $ Just rKey1
|
||||
@@ -339,15 +415,15 @@ testWithStoreLog at@(ATransport t) =
|
||||
writeTVar senderId1 sId1
|
||||
writeTVar notifierId nId
|
||||
Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, NSUB)
|
||||
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, SEND "hello")
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet h
|
||||
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, _SEND' "hello")
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet h
|
||||
(C.cbDecrypt dhShared (C.cbNonce mId1) msg1, Right "hello") #== "delivered from queue 1"
|
||||
Resp "" _ NMSG <- tGet h1
|
||||
Resp "" _ (NMSG _ _) <- tGet h1
|
||||
|
||||
(sId2, rId2, rKey2, dhShared2) <- createAndSecureQueue h sPub2
|
||||
atomically $ writeTVar senderId2 sId2
|
||||
Resp "cdab" _ OK <- signSendRecv h sKey2 ("cdab", sId2, SEND "hello too")
|
||||
Resp "" _ (MSG mId2 _ msg2) <- tGet h
|
||||
Resp "cdab" _ OK <- signSendRecv h sKey2 ("cdab", sId2, _SEND "hello too")
|
||||
Resp "" _ (MSG mId2 _ _ msg2) <- tGet h
|
||||
(C.cbDecrypt dhShared2 (C.cbNonce mId2) msg2, Right "hello too") #== "delivered from queue 2"
|
||||
|
||||
Resp "dabc" _ OK <- signSendRecv h rKey2 ("dabc", rId2, DEL)
|
||||
@@ -358,7 +434,7 @@ testWithStoreLog at@(ATransport t) =
|
||||
withSmpServerThreadOn at testPort . runTest t $ \h -> do
|
||||
sId1 <- readTVarIO senderId1
|
||||
-- fails if store log is disabled
|
||||
Resp "bcda" _ (ERR AUTH) <- signSendRecv h sKey1 ("bcda", sId1, SEND "hello")
|
||||
Resp "bcda" _ (ERR AUTH) <- signSendRecv h sKey1 ("bcda", sId1, _SEND "hello")
|
||||
pure ()
|
||||
|
||||
withSmpServerStoreLogOn at testPort . runTest t $ \h -> runClient t $ \h1 -> do
|
||||
@@ -369,13 +445,13 @@ testWithStoreLog at@(ATransport t) =
|
||||
sId1 <- readTVarIO senderId1
|
||||
nId <- readTVarIO notifierId
|
||||
Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, NSUB)
|
||||
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, SEND "hello")
|
||||
Resp "cdab" _ (MSG mId3 _ msg3) <- signSendRecv h rKey1 ("cdab", rId1, SUB)
|
||||
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, _SEND' "hello")
|
||||
Resp "cdab" _ (MSG mId3 _ _ msg3) <- signSendRecv h rKey1 ("cdab", rId1, SUB)
|
||||
(C.cbDecrypt dh1 (C.cbNonce mId3) msg3, Right "hello") #== "delivered from restored queue"
|
||||
Resp "" _ NMSG <- tGet h1
|
||||
Resp "" _ (NMSG _ _) <- tGet h1
|
||||
-- this queue is removed - not restored
|
||||
sId2 <- readTVarIO senderId2
|
||||
Resp "cdab" _ (ERR AUTH) <- signSendRecv h sKey2 ("cdab", sId2, SEND "hello too")
|
||||
Resp "cdab" _ (ERR AUTH) <- signSendRecv h sKey2 ("cdab", sId2, _SEND "hello too")
|
||||
pure ()
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
@@ -412,15 +488,15 @@ testRestoreMessages at@(ATransport t) =
|
||||
writeTVar recipientKey $ Just rKey
|
||||
writeTVar dhShared $ Just dh
|
||||
writeTVar senderId sId
|
||||
Resp "1" _ OK <- signSendRecv h sKey ("1", sId, SEND "hello")
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet h1
|
||||
Resp "1a" _ OK <- signSendRecv h1 rKey ("1a", rId, ACK)
|
||||
Resp "1" _ OK <- signSendRecv h sKey ("1", sId, _SEND "hello")
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet h1
|
||||
Resp "1a" _ OK <- signSendRecv h1 rKey ("1a", rId, ACK mId1)
|
||||
(C.cbDecrypt dh (C.cbNonce mId1) msg1, Right "hello") #== "message delivered"
|
||||
-- messages below are delivered after server restart
|
||||
sId <- readTVarIO senderId
|
||||
Resp "2" _ OK <- signSendRecv h sKey ("2", sId, SEND "hello 2")
|
||||
Resp "3" _ OK <- signSendRecv h sKey ("3", sId, SEND "hello 3")
|
||||
Resp "4" _ OK <- signSendRecv h sKey ("4", sId, SEND "hello 4")
|
||||
Resp "2" _ OK <- signSendRecv h sKey ("2", sId, _SEND "hello 2")
|
||||
Resp "3" _ OK <- signSendRecv h sKey ("3", sId, _SEND "hello 3")
|
||||
Resp "4" _ OK <- signSendRecv h sKey ("4", sId, _SEND "hello 4")
|
||||
pure ()
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 2
|
||||
@@ -430,11 +506,11 @@ testRestoreMessages at@(ATransport t) =
|
||||
rId <- readTVarIO recipientId
|
||||
Just rKey <- readTVarIO recipientKey
|
||||
Just dh <- readTVarIO dhShared
|
||||
Resp "2" _ (MSG mId2 _ msg2) <- signSendRecv h rKey ("2", rId, SUB)
|
||||
Resp "2" _ (MSG mId2 _ _ msg2) <- signSendRecv h rKey ("2", rId, SUB)
|
||||
(C.cbDecrypt dh (C.cbNonce mId2) msg2, Right "hello 2") #== "restored message delivered"
|
||||
Resp "3" _ (MSG mId3 _ msg3) <- signSendRecv h rKey ("3", rId, ACK)
|
||||
Resp "3" _ (MSG mId3 _ _ msg3) <- signSendRecv h rKey ("3", rId, ACK mId2)
|
||||
(C.cbDecrypt dh (C.cbNonce mId3) msg3, Right "hello 3") #== "restored message delivered"
|
||||
Resp "4" _ (MSG mId4 _ msg4) <- signSendRecv h rKey ("4", rId, ACK)
|
||||
Resp "4" _ (MSG mId4 _ _ msg4) <- signSendRecv h rKey ("4", rId, ACK mId3)
|
||||
(C.cbDecrypt dh (C.cbNonce mId4) msg4, Right "hello 4") #== "restored message delivered"
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
@@ -445,8 +521,8 @@ testRestoreMessages at@(ATransport t) =
|
||||
rId <- readTVarIO recipientId
|
||||
Just rKey <- readTVarIO recipientKey
|
||||
Just dh <- readTVarIO dhShared
|
||||
Resp "4" _ (MSG mId4 _ msg4) <- signSendRecv h rKey ("4", rId, SUB)
|
||||
Resp "5" _ OK <- signSendRecv h rKey ("5", rId, ACK)
|
||||
Resp "4" _ (MSG mId4 _ _ msg4) <- signSendRecv h rKey ("4", rId, SUB)
|
||||
Resp "5" _ OK <- signSendRecv h rKey ("5", rId, ACK mId4)
|
||||
(C.cbDecrypt dh (C.cbNonce mId4) msg4, Right "hello 4") #== "restored message delivered"
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
@@ -501,11 +577,11 @@ testTiming (ATransport t) =
|
||||
(sPub, sKey) <- generateKeys goodKeySize
|
||||
Resp "dabc" _ OK <- signSendRecv rh rKey ("dabc", rId, KEY sPub)
|
||||
|
||||
Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, SEND "hello")
|
||||
Resp "" _ (MSG mId _ msg) <- tGet rh
|
||||
Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, _SEND "hello")
|
||||
Resp "" _ (MSG mId _ _ msg) <- tGet rh
|
||||
(dec mId msg, Right "hello") #== "delivered from queue"
|
||||
|
||||
runTimingTest sh badKey sId $ SEND "hello"
|
||||
runTimingTest sh badKey sId $ _SEND "hello"
|
||||
where
|
||||
generateKeys = \case
|
||||
32 -> C.generateSignatureKeyPair C.SEd25519
|
||||
@@ -535,22 +611,33 @@ testMessageNotifications (ATransport t) =
|
||||
smpTest4 t $ \rh sh nh1 nh2 -> do
|
||||
(sId, rId, rKey, dhShared) <- createAndSecureQueue rh sPub
|
||||
let dec nonce = C.cbDecrypt dhShared (C.cbNonce nonce)
|
||||
Resp "1" _ (NID nId) <- signSendRecv rh rKey ("1", rId, NKEY nPub)
|
||||
(rcvNtfPubDhKey, _) <- C.generateKeyPair'
|
||||
Resp "1" _ (NID nId' _) <- signSendRecv rh rKey ("1", rId, NKEY nPub rcvNtfPubDhKey)
|
||||
Resp "1a" _ (NID nId _) <- signSendRecv rh rKey ("1a", rId, NKEY nPub rcvNtfPubDhKey)
|
||||
nId' `shouldNotBe` nId
|
||||
Resp "2" _ OK <- signSendRecv nh1 nKey ("2", nId, NSUB)
|
||||
Resp "3" _ OK <- signSendRecv sh sKey ("3", sId, SEND "hello")
|
||||
Resp "" _ (MSG mId1 _ msg1) <- tGet rh
|
||||
Resp "3" _ OK <- signSendRecv sh sKey ("3", sId, _SEND' "hello")
|
||||
Resp "" _ (MSG mId1 _ _ msg1) <- tGet rh
|
||||
(dec mId1 msg1, Right "hello") #== "delivered from queue"
|
||||
Resp "3a" _ OK <- signSendRecv rh rKey ("3a", rId, ACK)
|
||||
Resp "" _ NMSG <- tGet nh1
|
||||
Resp "3a" _ OK <- signSendRecv rh rKey ("3a", rId, ACK mId1)
|
||||
Resp "" _ (NMSG _ _) <- tGet nh1
|
||||
Resp "4" _ OK <- signSendRecv nh2 nKey ("4", nId, NSUB)
|
||||
Resp "" _ END <- tGet nh1
|
||||
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, SEND "hello again")
|
||||
Resp "" _ (MSG mId2 _ msg2) <- tGet rh
|
||||
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, _SEND' "hello again")
|
||||
Resp "" _ (MSG mId2 _ _ msg2) <- tGet rh
|
||||
Resp "5a" _ OK <- signSendRecv rh rKey ("5a", rId, ACK mId2)
|
||||
(dec mId2 msg2, Right "hello again") #== "delivered from queue again"
|
||||
Resp "" _ NMSG <- tGet nh2
|
||||
Resp "" _ (NMSG _ _) <- tGet nh2
|
||||
1000 `timeout` tGet @BrokerMsg nh1 >>= \case
|
||||
Nothing -> return ()
|
||||
Nothing -> pure ()
|
||||
Just _ -> error "nothing else should be delivered to the 1st notifier's TCP connection"
|
||||
Resp "6" _ OK <- signSendRecv rh rKey ("6", rId, NDEL)
|
||||
Resp "7" _ OK <- signSendRecv sh sKey ("7", sId, _SEND' "hello there")
|
||||
Resp "" _ (MSG mId3 _ _ msg3) <- tGet rh
|
||||
(dec mId3 msg3, Right "hello there") #== "delivered from queue again"
|
||||
1000 `timeout` tGet @BrokerMsg nh2 >>= \case
|
||||
Nothing -> pure ()
|
||||
Just _ -> error "nothing else should be delivered to the 2nd notifier's TCP connection"
|
||||
|
||||
testMsgExpireOnSend :: forall c. Transport c => TProxy c -> Spec
|
||||
testMsgExpireOnSend t =
|
||||
@@ -561,11 +648,11 @@ testMsgExpireOnSend t =
|
||||
testSMPClient @c $ \sh -> do
|
||||
(sId, rId, rKey, dhShared) <- testSMPClient @c $ \rh -> createAndSecureQueue rh sPub
|
||||
let dec nonce = C.cbDecrypt dhShared (C.cbNonce nonce)
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, SEND "hello (should expire)")
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello (should expire)")
|
||||
threadDelay 2500000
|
||||
Resp "2" _ OK <- signSendRecv sh sKey ("2", sId, SEND "hello (should NOT expire)")
|
||||
Resp "2" _ OK <- signSendRecv sh sKey ("2", sId, _SEND "hello (should NOT expire)")
|
||||
testSMPClient @c $ \rh -> do
|
||||
Resp "3" _ (MSG mId _ msg) <- signSendRecv rh rKey ("3", rId, SUB)
|
||||
Resp "3" _ (MSG mId _ _ msg) <- signSendRecv rh rKey ("3", rId, SUB)
|
||||
(dec mId msg, Right "hello (should NOT expire)") #== "delivered"
|
||||
1000 `timeout` tGet @BrokerMsg rh >>= \case
|
||||
Nothing -> return ()
|
||||
@@ -579,7 +666,7 @@ testMsgExpireOnInterval t =
|
||||
withSmpServerConfigOn (ATransport t) cfg' testPort $ \_ ->
|
||||
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)")
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello (should expire)")
|
||||
threadDelay 2500000
|
||||
testSMPClient @c $ \rh -> do
|
||||
Resp "2" _ OK <- signSendRecv rh rKey ("2", rId, SUB)
|
||||
@@ -596,10 +683,10 @@ testMsgNOTExpireOnInterval t =
|
||||
testSMPClient @c $ \sh -> do
|
||||
(sId, rId, rKey, dhShared) <- testSMPClient @c $ \rh -> createAndSecureQueue rh sPub
|
||||
let dec nonce = C.cbDecrypt dhShared (C.cbNonce nonce)
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, SEND "hello (should NOT expire)")
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello (should NOT expire)")
|
||||
threadDelay 2500000
|
||||
testSMPClient @c $ \rh -> do
|
||||
Resp "2" _ (MSG mId _ msg) <- signSendRecv rh rKey ("2", rId, SUB)
|
||||
Resp "2" _ (MSG mId _ _ msg) <- signSendRecv rh rKey ("2", rId, SUB)
|
||||
(dec mId msg, Right "hello (should NOT expire)") #== "delivered"
|
||||
1000 `timeout` tGet @BrokerMsg rh >>= \case
|
||||
Nothing -> return ()
|
||||
@@ -629,13 +716,17 @@ syntaxTests (ATransport t) = do
|
||||
it "no signature" $ ("", "abcd", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "abcd", "12345678", ERR $ CMD NO_AUTH)
|
||||
it "no queue ID" $ (sampleSig, "bcda", "", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "", ERR $ CMD NO_AUTH)
|
||||
noParamsSyntaxTest "SUB" SUB_
|
||||
noParamsSyntaxTest "ACK" ACK_
|
||||
noParamsSyntaxTest "OFF" OFF_
|
||||
noParamsSyntaxTest "DEL" DEL_
|
||||
describe "SEND" $ do
|
||||
it "valid syntax" $ (sampleSig, "cdab", "12345678", (SEND_, ' ', "hello" :: ByteString)) >#> ("", "cdab", "12345678", ERR AUTH)
|
||||
it "valid syntax" $ (sampleSig, "cdab", "12345678", (SEND_, ' ', noMsgFlags, ' ', "hello" :: ByteString)) >#> ("", "cdab", "12345678", ERR AUTH)
|
||||
it "no parameters" $ (sampleSig, "abcd", "12345678", SEND_) >#> ("", "abcd", "12345678", ERR $ CMD SYNTAX)
|
||||
it "no queue ID" $ (sampleSig, "bcda", "", (SEND_, ' ', "hello" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_ENTITY)
|
||||
it "no queue ID" $ (sampleSig, "bcda", "", (SEND_, ' ', noMsgFlags, ' ', "hello" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_ENTITY)
|
||||
describe "ACK" $ do
|
||||
it "valid syntax" $ (sampleSig, "cdab", "12345678", (ACK_, ' ', "1234" :: ByteString)) >#> ("", "cdab", "12345678", ERR AUTH)
|
||||
it "no parameters" $ (sampleSig, "abcd", "12345678", ACK_) >#> ("", "abcd", "12345678", ERR $ CMD SYNTAX)
|
||||
it "no queue ID" $ (sampleSig, "bcda", "", (ACK_, ' ', "1234" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_AUTH)
|
||||
it "no signature" $ ("", "cdab", "12345678", (ACK_, ' ', "1234" :: ByteString)) >#> ("", "cdab", "12345678", ERR $ CMD NO_AUTH)
|
||||
describe "PING" $ do
|
||||
it "valid syntax" $ ("", "abcd", "", PING_) >#> ("", "abcd", "", PONG)
|
||||
describe "broker response not allowed" $ do
|
||||
|
||||
+1
-4
@@ -1,7 +1,6 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
import AgentTests (agentTests)
|
||||
import AgentTests.NotificationTests (notificationTests)
|
||||
import CoreTests.EncodingTests
|
||||
import CoreTests.ProtocolErrorTests
|
||||
import CoreTests.VersionRangeTests
|
||||
@@ -25,8 +24,6 @@ main = do
|
||||
describe "Version range" versionRangeTests
|
||||
describe "SMP server via TLS" $ serverTests (transport @TLS)
|
||||
describe "SMP server via WebSockets" $ serverTests (transport @WS)
|
||||
describe "Notifications server" $ do
|
||||
ntfServerTests (transport @TLS)
|
||||
notificationTests (transport @TLS)
|
||||
describe "Notifications server" $ ntfServerTests (transport @TLS)
|
||||
describe "SMP client agent" $ agentTests (transport @TLS)
|
||||
removeDirectoryRecursive "tests/tmp"
|
||||
|
||||
Reference in New Issue
Block a user