Compare commits

..
Author SHA1 Message Date
John Roberts 8c298728e2 wip 2022-03-12 22:57:07 +04:00
Efim Poberezkin cca8ac5a58 init, debugging (some data is being written to db) 2022-02-04 13:59:14 +04:00
Efim Poberezkin b1d2d45947 compiles 2022-02-04 12:45:05 +04:00
Efim Poberezkin c9c6d2b2d3 some instances 2022-02-03 17:57:09 +04:00
Efim Poberezkin 85c09d1703 re-trigger build 2022-02-03 17:43:01 +04:00
Efim Poberezkin 08b43b42a0 test compilation 2022-02-03 17:20:49 +04:00
Efim Poberezkin 4980db932d use posgres fork 2022-02-03 15:06:25 +04:00
Efim Poberezkin b2fbab5b0f Postgres POC (duplicated SQLite code) 2022-02-02 12:08:07 +04:00
49 changed files with 2152 additions and 1189 deletions
-37
View File
@@ -1,58 +1,24 @@
# 1.1.0
SMP server:
- message TTL and periodic deletion of old messages
- configuration to prevent creation of the new queues
SMP agent:
- asynchronous connection handshake
- configurable SMP servers at run-time
- use TCP keep-alive for connection stability
- improve stability of connection subscriptions
- auto-vacuum DB to remove deleted records
# 1.0.3
SMP server:
- Reduce server message queue quota to 128 messages.
SMP agent:
- Add "yes to migrations" option.
- Make new SMP client attempt to reconnect on network error.
- Reduce connection handshake expiration to 2 days.
JSON encoding of types used in simplex-chat, some other minor adjustments.
# 1.0.2 # 1.0.2
General: General:
- Enable TLS 1.3 parameters for TLS handshake (server and client). - Enable TLS 1.3 parameters for TLS handshake (server and client).
- Switch from hs-tls fork to original repo now that it supports getFinished and getPeerFinished APIs for both TLS 1.2 and TLS 1.3. - Switch from hs-tls fork to original repo now that it supports getFinished and getPeerFinished APIs for both TLS 1.2 and TLS 1.3.
SMP server: SMP server:
- Perform TLS handshake in a separate thread per-connection. - Perform TLS handshake in a separate thread per-connection.
SMP agent: SMP agent:
- Cease attempts to send HELLO after one week timeout. - Cease attempts to send HELLO after one week timeout.
- Coalesce requests to connect to SMP servers, to have 1 connection per server. - Coalesce requests to connect to SMP servers, to have 1 connection per server.
# 1.0.1 # 1.0.1
SMP server: SMP server:
- Explicitly set line buffering in stdout/stderr to log each line when output is redirected to files. - Explicitly set line buffering in stdout/stderr to log each line when output is redirected to files.
# 1.0.0 # 1.0.0
Security and privacy improvements: Security and privacy improvements:
- Faster and more secure 2-layer E2E encryption with additional encryption layer between servers and recipients: - Faster and more secure 2-layer E2E encryption with additional encryption layer between servers and recipients:
- application messages in each duplex connection (managed by SMP agents - see [overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md)) are encrypted using [double-ratchet algorithm](https://www.signal.org/docs/specifications/doubleratchet/), providing forward secrecy and break-in recovery. This layer uses two Curve448 keys per client for [X3DH key agreement](https://www.signal.org/docs/specifications/x3dh/), SHA512 based HKDFs and AES-GCM AEAD encryption. - application messages in each duplex connection (managed by SMP agents - see [overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md)) are encrypted using [double-ratchet algorithm](https://www.signal.org/docs/specifications/doubleratchet/), providing forward secrecy and break-in recovery. This layer uses two Curve448 keys per client for [X3DH key agreement](https://www.signal.org/docs/specifications/x3dh/), SHA512 based HKDFs and AES-GCM AEAD encryption.
- SMP client messages are additionally E2E encrypted in each SMP queue to avoid cipher-text correlation of messages sent via multiple redundant queues (that will be supported soon). This and the next layer use [NaCl crypto_box algorithm](https://nacl.cr.yp.to/index.html) with XSalsa20Poly1305 cipher and Curve25519 keys for DH key agreement. - SMP client messages are additionally E2E encrypted in each SMP queue to avoid cipher-text correlation of messages sent via multiple redundant queues (that will be supported soon). This and the next layer use [NaCl crypto_box algorithm](https://nacl.cr.yp.to/index.html) with XSalsa20Poly1305 cipher and Curve25519 keys for DH key agreement.
@@ -65,16 +31,13 @@ Security and privacy improvements:
- Server identity verification via server offline certificate fingerprints included in SMP server addresses. - Server identity verification via server offline certificate fingerprints included in SMP server addresses.
New functionality: New functionality:
- Support for notification servers with new SMP commands: `NKEY`/`NID`, `NSUB`/`NMSG`. - Support for notification servers with new SMP commands: `NKEY`/`NID`, `NSUB`/`NMSG`.
Efficiency improvements: Efficiency improvements:
- Binary protocol encodings to reduce overhead from circa 15% to approximately 3.7% of transmitted application message size, with only 2.2% overhead for SMP protocol messages. - Binary protocol encodings to reduce overhead from circa 15% to approximately 3.7% of transmitted application message size, with only 2.2% overhead for SMP protocol messages.
- More performant cryptographic algorithms. - More performant cryptographic algorithms.
For more information about SimpleX: For more information about SimpleX:
- [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md). - [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
- [SimpleX chat v1 announcement](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md). - [SimpleX chat v1 announcement](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md).
+2 -22
View File
@@ -27,9 +27,9 @@ SimpleXMQ is implemented in Haskell - it benefits from robust software transacti
### SMP server ### SMP server
[SMP server](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs) can be run on any Linux distribution, including low power/low memory devices. OpenSSL library is required for initialization. [SMP server](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs) can be run on any Linux distribution without any dependencies, including low power/low memory devices.
To initialize the server use `smp-server init -n <fqdn>` (or `smp-server init --ip <ip>` for IP based address) command - it will generate keys and certificates for TLS transport. The fingerprint of offline certificate is used as part of the server address to protect client/server connection against man-in-the-middle attacks: `smp://<fingerprint>@<hostname>[:5223]`. To initialize the server use `smp-server init` command - it will generate keys and certificates for TLS transport. The fingerprint of offline certificate is used as part of the server address to protect client/server connection against man-in-the-middle attacks: `smp://<fingerprint>@<hostname>[:5223]`.
SMP server uses in-memory persistence with an optional append-only log of created queues that allows to re-start the server without losing the connections. This log is compacted on every server restart, permanently removing suspended and removed queues. SMP server uses in-memory persistence with an optional append-only log of created queues that allows to re-start the server without losing the connections. This log is compacted on every server restart, permanently removing suspended and removed queues.
@@ -87,26 +87,6 @@ You can either run your own SMP server locally or deploy using [Linode StackScri
It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI. It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.
## Deploy SMP server on Linux
You can run your SMP server as a Linux process, optionally using a service manager for booting and restarts.
- For Ubuntu you can download a binary from [the latest release](https://github.com/simplex-chat/simplexmq/releases).
If you're using other Linux distribution and the binary is incompatible with it, you can build from source using [Haskell stack](https://docs.haskellstack.org/en/stable/README/):
```shell
curl -sSL https://get.haskellstack.org/ | sh
...
stack install
```
- Initialize SMP server with `smp-server init [-l] -n <fqdn>` or `smp-server init [-l] --ip <ip>` - depending on how you initialize it, either FQDN or IP will be used for server's address.
- Run `smp-server start` to start SMP server, or you can configure a service manager to run it as a service.
See [this section](#smp-server) for more information. Run `smp-server -h` and `smp-server init -h` for explanation of commands and options.
[<img alt="Linode" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014) [<img alt="Linode" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)
## Deploy SMP server on Linode ## Deploy SMP server on Linode
+2 -2
View File
@@ -6,12 +6,12 @@ module Main where
import Control.Logger.Simple import Control.Logger.Simple
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Env.Postgres
import Simplex.Messaging.Agent.Server (runSMPAgent) import Simplex.Messaging.Agent.Server (runSMPAgent)
import Simplex.Messaging.Transport (TLS, Transport (..)) import Simplex.Messaging.Transport (TLS, Transport (..))
cfg :: AgentConfig cfg :: AgentConfig
cfg = defaultAgentConfig {initialSMPServers = L.fromList ["smp://bU0K-bRg24xWW__lS0umO1Zdw_SXqpJNtm1_RrPLViE=@localhost:5223"]} cfg = defaultAgentConfig {smpServers = L.fromList ["smp://bU0K-bRg24xWW__lS0umO1Zdw_SXqpJNtm1_RrPLViE=@localhost:5223"]}
logCfg :: LogConfig logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
+6 -9
View File
@@ -3,7 +3,6 @@
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
@@ -132,7 +131,8 @@ cliCommandP =
<*> strOption <*> strOption
( long "ip" ( long "ip"
<> help <> help
"Server IP address, used as Common Name for TLS online certificate if FQDN is not supplied" "Server IP address used as Subject Alternative Name for TLS online certificate, \
\also used as Common Name if FQDN is not supplied"
<> value "127.0.0.1" <> value "127.0.0.1"
<> showDefault <> showDefault
<> metavar "IP" <> metavar "IP"
@@ -140,7 +140,7 @@ cliCommandP =
<*> (optional . strOption) <*> (optional . strOption)
( long "fqdn" ( long "fqdn"
<> short 'n' <> short 'n'
<> help "Server FQDN used as Common Name for TLS online certificate" <> help "Server FQDN used as Common Name and Subject Alternative Name for TLS online certificate"
<> showDefault <> showDefault
<> metavar "FQDN" <> metavar "FQDN"
) )
@@ -277,17 +277,14 @@ runServer IniOptions {enableStoreLog, port, enableWebsockets} = do
ServerConfig ServerConfig
{ transports = (port, transport @TLS) : [("80", transport @WS) | enableWebsockets], { transports = (port, transport @TLS) : [("80", transport @WS) | enableWebsockets],
tbqSize = 16, tbqSize = 16,
serverTbqSize = 64, serverTbqSize = 128,
msgQueueQuota = 128, msgQueueQuota = 256,
queueIdBytes = 24, queueIdBytes = 24,
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20 msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
caCertificateFile = caCrtFile, caCertificateFile = caCrtFile,
privateKeyFile = serverKeyFile, privateKeyFile = serverKeyFile,
certificateFile = serverCrtFile, certificateFile = serverCrtFile,
storeLog, storeLog
allowNewQueues = True,
messageTTL = Just $ 7 * 86400, -- 7 days
expireMessagesInterval = Just 21600_000000 -- microseconds, 6 hours
} }
openStoreLog :: IO (Maybe (StoreLog 'ReadMode)) openStoreLog :: IO (Maybe (StoreLog 'ReadMode))
+1 -1
View File
@@ -2,5 +2,5 @@ packages: .
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/aeson.git location: git://github.com/simplex-chat/aeson.git
tag: 3eb66f9a68f103b5f1489382aad89f5712a64db7 tag: 3eb66f9a68f103b5f1489382aad89f5712a64db7
+4 -14
View File
@@ -1,7 +1,7 @@
name: simplexmq name: simplexmq
version: 1.1.0 version: 1.0.2
synopsis: SimpleXMQ message broker synopsis: SimpleXMQ message broker
description: | description: |
This package includes <./docs/Simplex-Messaging-Server.html server>, This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and <./docs/Simplex-Messaging-Client.html client> and
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols: <./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
@@ -45,8 +45,9 @@ dependencies:
- iso8601-time == 0.1.* - iso8601-time == 0.1.*
- memory == 0.15.* - memory == 0.15.*
- mtl == 2.2.* - mtl == 2.2.*
- network == 3.1.2.* - network == 3.1.*
- network-transport == 0.5.* - network-transport == 0.5.*
- postgresql-simple == 0.6.*
- QuickCheck == 2.14.* - QuickCheck == 2.14.*
- random >= 1.1 && < 1.3 - random >= 1.1 && < 1.3
- simple-logger == 0.1.* - simple-logger == 0.1.*
@@ -64,17 +65,6 @@ dependencies:
- x509-store == 1.6.* - x509-store == 1.6.*
- x509-validation == 1.6.* - x509-validation == 1.6.*
flags:
swift:
description: Enable swift JSON format
manual: True
default: False
when:
- condition: flag(swift)
ghc-options:
- -DswiftJSON
library: library:
source-dirs: src source-dirs: src
@@ -1,21 +0,0 @@
# SMP confirmation timeout recovery
## Problem
When sending an SMP confirmation a network timeout can lead to the following race condition:
- server receives the confirmation while the joining party fails to receive the server's response;
- joining party deletes the connection together with credentials sent in the confirmation for securing the queue;
- initiating party will receive the confirmation from the server and secure the queue;
- on subsequent attempt to join via the same invitation link initiating party will generate new credentials and fail authorization.
This renders the joining party permanently unable to join via that invitation link and complete the connection.
## Solution
A possible solution is to keep and try to reuse same credentials on subsequent attempts:
- joining party has to remember invitation link when saving the connection;
- if SMP confirmation fails due to network timeout joining party doesn't delete the connection and keeps the credentials;
- when joining, joining party checks whether such invitation link was already used for a connection, if yes:
- joining party tries to send SMP confirmation with the same credentials;
- if this SMP confirmation fails with authorization error (for example it can happen due to race condition explained above) joining party tries to send HELLO message;
- if HELLO message fails with authorization error (it can happen if connection was deleted or secured with different credentials), the recovery is no longer possible and connection can be deleted.
@@ -16,8 +16,3 @@ brew install hashicorp/tap/packer
cd ./scripts/smp-server-digitalocean-droplet cd ./scripts/smp-server-digitalocean-droplet
DIGITALOCEAN_TOKEN=$YOUR_TOKEN packer build -on-error=ask -color=false ./marketplace-image.json DIGITALOCEAN_TOKEN=$YOUR_TOKEN packer build -on-error=ask -color=false ./marketplace-image.json
``` ```
**TODO** (see Linode script)
- Increase file descriptors limit
- Configure Restart for systemd service
-10
View File
@@ -44,12 +44,6 @@ ufw allow ssh
ufw allow https ufw allow https
ufw allow 5223 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 # Download latest release
bin_dir="/opt/simplex/bin" bin_dir="/opt/simplex/bin"
binary="$bin_dir/smp-server" binary="$bin_dir/smp-server"
@@ -157,10 +151,6 @@ Description=SMP server
[Service] [Service]
Type=simple Type=simple
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1" ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
Restart=always
RestartSec=10
LimitNOFILE=1000000
LimitNOFILESoft=1000000
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+13 -21
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplexmq name: simplexmq
version: 1.1.0 version: 1.0.2
synopsis: SimpleXMQ message broker synopsis: SimpleXMQ message broker
description: This package includes <./docs/Simplex-Messaging-Server.html server>, description: This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and <./docs/Simplex-Messaging-Client.html client> and
@@ -27,25 +27,23 @@ extra-source-files:
README.md README.md
CHANGELOG.md CHANGELOG.md
flag swift
description: Enable swift JSON format
manual: True
default: False
library library
exposed-modules: exposed-modules:
Simplex.Messaging.Agent Simplex.Messaging.Agent
Simplex.Messaging.Agent.Client Simplex.Messaging.Agent.Client
Simplex.Messaging.Agent.Env.Postgres
Simplex.Messaging.Agent.Env.SQLite Simplex.Messaging.Agent.Env.SQLite
Simplex.Messaging.Agent.Protocol Simplex.Messaging.Agent.Protocol
Simplex.Messaging.Agent.QueryString Simplex.Messaging.Agent.QueryString
Simplex.Messaging.Agent.RetryInterval Simplex.Messaging.Agent.RetryInterval
Simplex.Messaging.Agent.Server Simplex.Messaging.Agent.Server
Simplex.Messaging.Agent.Store Simplex.Messaging.Agent.Store
Simplex.Messaging.Agent.Store.Postgres
Simplex.Messaging.Agent.Store.Postgres.Migrations
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20220202_initial
Simplex.Messaging.Agent.Store.SQLite Simplex.Messaging.Agent.Store.SQLite
Simplex.Messaging.Agent.Store.SQLite.Migrations Simplex.Messaging.Agent.Store.SQLite.Migrations
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
Simplex.Messaging.Client Simplex.Messaging.Client
Simplex.Messaging.Crypto Simplex.Messaging.Crypto
Simplex.Messaging.Crypto.Ratchet Simplex.Messaging.Crypto.Ratchet
@@ -60,10 +58,8 @@ library
Simplex.Messaging.Server.QueueStore Simplex.Messaging.Server.QueueStore
Simplex.Messaging.Server.QueueStore.STM Simplex.Messaging.Server.QueueStore.STM
Simplex.Messaging.Server.StoreLog Simplex.Messaging.Server.StoreLog
Simplex.Messaging.TMap
Simplex.Messaging.Transport Simplex.Messaging.Transport
Simplex.Messaging.Transport.Client Simplex.Messaging.Transport.Client
Simplex.Messaging.Transport.KeepAlive
Simplex.Messaging.Transport.Server Simplex.Messaging.Transport.Server
Simplex.Messaging.Transport.WebSockets Simplex.Messaging.Transport.WebSockets
Simplex.Messaging.Util Simplex.Messaging.Util
@@ -98,8 +94,9 @@ library
, iso8601-time ==0.1.* , iso8601-time ==0.1.*
, memory ==0.15.* , memory ==0.15.*
, mtl ==2.2.* , mtl ==2.2.*
, network ==3.1.2.* , network ==3.1.*
, network-transport ==0.5.* , network-transport ==0.5.*
, postgresql-simple ==0.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, simple-logger ==0.1.* , simple-logger ==0.1.*
, sqlite-simple ==0.4.* , sqlite-simple ==0.4.*
@@ -115,8 +112,6 @@ library
, x509 ==1.7.* , x509 ==1.7.*
, x509-store ==1.6.* , x509-store ==1.6.*
, x509-validation ==1.6.* , x509-validation ==1.6.*
if flag(swift)
ghc-options: -DswiftJSON
default-language: Haskell2010 default-language: Haskell2010
executable smp-agent executable smp-agent
@@ -151,8 +146,9 @@ executable smp-agent
, iso8601-time ==0.1.* , iso8601-time ==0.1.*
, memory ==0.15.* , memory ==0.15.*
, mtl ==2.2.* , mtl ==2.2.*
, network ==3.1.2.* , network ==3.1.*
, network-transport ==0.5.* , network-transport ==0.5.*
, postgresql-simple ==0.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplexmq , simplexmq
@@ -169,8 +165,6 @@ executable smp-agent
, x509 ==1.7.* , x509 ==1.7.*
, x509-store ==1.6.* , x509-store ==1.6.*
, x509-validation ==1.6.* , x509-validation ==1.6.*
if flag(swift)
ghc-options: -DswiftJSON
default-language: Haskell2010 default-language: Haskell2010
executable smp-server executable smp-server
@@ -206,9 +200,10 @@ executable smp-server
, iso8601-time ==0.1.* , iso8601-time ==0.1.*
, memory ==0.15.* , memory ==0.15.*
, mtl ==2.2.* , mtl ==2.2.*
, network ==3.1.2.* , network ==3.1.*
, network-transport ==0.5.* , network-transport ==0.5.*
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, postgresql-simple ==0.6.*
, process ==1.6.* , process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, simple-logger ==0.1.* , simple-logger ==0.1.*
@@ -226,8 +221,6 @@ executable smp-server
, x509 ==1.7.* , x509 ==1.7.*
, x509-store ==1.6.* , x509-store ==1.6.*
, x509-validation ==1.6.* , x509-validation ==1.6.*
if flag(swift)
ghc-options: -DswiftJSON
default-language: Haskell2010 default-language: Haskell2010
test-suite smp-server-test test-suite smp-server-test
@@ -277,8 +270,9 @@ test-suite smp-server-test
, iso8601-time ==0.1.* , iso8601-time ==0.1.*
, memory ==0.15.* , memory ==0.15.*
, mtl ==2.2.* , mtl ==2.2.*
, network ==3.1.2.* , network ==3.1.*
, network-transport ==0.5.* , network-transport ==0.5.*
, postgresql-simple ==0.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplexmq , simplexmq
@@ -296,6 +290,4 @@ test-suite smp-server-test
, x509 ==1.7.* , x509 ==1.7.*
, x509-store ==1.6.* , x509-store ==1.6.*
, x509-validation ==1.6.* , x509-validation ==1.6.*
if flag(swift)
ghc-options: -DswiftJSON
default-language: Haskell2010 default-language: Haskell2010
+99 -129
View File
@@ -47,7 +47,6 @@ module Simplex.Messaging.Agent
ackMessage, ackMessage,
suspendConnection, suspendConnection,
deleteConnection, deleteConnection,
setSMPServers,
logConnection, logConnection,
) )
where where
@@ -71,19 +70,18 @@ import Data.Time.Clock
import Data.Time.Clock.System (systemToUTCTime) import Data.Time.Clock.System (systemToUTCTime)
import Database.SQLite.Simple (SQLError) import Database.SQLite.Simple (SQLError)
import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Client
import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Env.Postgres
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Agent.Store.Postgres (PostgresStore)
import Simplex.Messaging.Client (SMPClient (..), SMPServerTransmission) import Simplex.Messaging.Client (SMPServerTransmission)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Ratchet as CR import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (parse) import Simplex.Messaging.Parsers (parse)
import Simplex.Messaging.Protocol (MsgBody) import Simplex.Messaging.Protocol (MsgBody)
import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.Protocol as SMP
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (bshow, liftError, tryError, unlessM) import Simplex.Messaging.Util (bshow, liftError, tryError, unlessM)
import Simplex.Messaging.Version import Simplex.Messaging.Version
import System.Random (randomR) import System.Random (randomR)
@@ -145,10 +143,6 @@ suspendConnection c = withAgentEnv c . suspendConnection' c
deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
deleteConnection c = withAgentEnv c . deleteConnection' c deleteConnection c = withAgentEnv c . deleteConnection' c
-- | Change servers to be used for creating new queues
setSMPServers :: AgentErrorMonad m => AgentClient -> NonEmpty SMPServer -> m ()
setSMPServers c = withAgentEnv c . setSMPServers' c
withAgentEnv :: AgentClient -> ReaderT Env m a -> m a withAgentEnv :: AgentClient -> ReaderT Env m a -> m a
withAgentEnv c = (`runReaderT` agentEnv c) withAgentEnv c = (`runReaderT` agentEnv c)
@@ -178,18 +172,22 @@ client c@AgentClient {rcvQ, subQ} = forever $ do
withStore :: withStore ::
AgentMonad m => AgentMonad m =>
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) -> (forall m'. (MonadUnliftIO m', MonadError StoreError m') => PostgresStore -> m' a) ->
m a m a
withStore action = do withStore action = do
st <- asks store st <- asks store
runExceptT (action st `E.catch` handleInternal) >>= \case runExceptT (action st `E.catch` handleInternal) >>= \case
Right c -> return c Right c -> return c
Left e -> throwError $ storeError e Left e -> do
liftIO $ print e
throwError $ storeError e
where where
-- TODO when parsing exception happens in store, the agent hangs; -- TODO when parsing exception happens in store, the agent hangs;
-- changing SQLError to SomeException does not help -- changing SQLError to SomeException does not help
handleInternal :: (MonadError StoreError m') => SQLError -> m' a handleInternal :: (MonadUnliftIO m', MonadError StoreError m') => SQLError -> m' a
handleInternal e = throwError . SEInternal $ bshow e handleInternal e = do
liftIO $ print e
throwError . SEInternal $ bshow e
storeError :: StoreError -> AgentErrorType storeError :: StoreError -> AgentErrorType
storeError = \case storeError = \case
SEConnNotFound -> CONN NOT_FOUND SEConnNotFound -> CONN NOT_FOUND
@@ -215,7 +213,7 @@ processCommand c (connId, cmd) = case cmd of
newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequestUri c) newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequestUri c)
newConn c connId cMode = do newConn c connId cMode = do
srv <- getSMPServer c srv <- getSMPServer
(rq, qUri) <- newRcvQueue c srv (rq, qUri) <- newRcvQueue c srv
g <- asks idsDrg g <- asks idsDrg
let cData = ConnData {connId} let cData = ConnData {connId}
@@ -240,21 +238,19 @@ joinConn c connId (CRInvitationUri (ConnReqUriData _ agentVRange (qUri :| _)) e2
(pk1, pk2, e2eSndParams) <- liftIO . CR.generateE2EParams $ version e2eRcvParams (pk1, pk2, e2eSndParams) <- liftIO . CR.generateE2EParams $ version e2eRcvParams
(_, rcDHRs) <- liftIO C.generateKeyPair' (_, rcDHRs) <- liftIO C.generateKeyPair'
let rc = CR.initSndRatchet rcDHRr rcDHRs $ CR.x3dhSnd pk1 pk2 e2eRcvParams let rc = CR.initSndRatchet rcDHRr rcDHRs $ CR.x3dhSnd pk1 pk2 e2eRcvParams
sq <- newSndQueue qInfo (sq, smpConf) <- newSndQueue qInfo cInfo
g <- asks idsDrg g <- asks idsDrg
let cData = ConnData {connId} let cData = ConnData {connId}
connId' <- withStore $ \st -> do connId' <- withStore $ \st -> do
liftIO $ print "before: createSndConn st g cData sq"
connId' <- createSndConn st g cData sq connId' <- createSndConn st g cData sq
liftIO $ print "before: createRatchet st connId' rc"
createRatchet st connId' rc createRatchet st connId' rc
liftIO $ print "after: createRatchet st connId' rc"
pure connId' pure connId'
tryError (confirmQueue c connId' sq cInfo $ Just e2eSndParams) >>= \case confirmQueue c connId' sq smpConf $ Just e2eSndParams
Right _ -> do void $ enqueueMessage c connId' sq HELLO
void $ enqueueMessage c connId' sq HELLO pure connId'
pure connId'
Left e -> do
-- TODO recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md
withStore (`deleteConn` connId')
throwError e
_ -> throwError $ AGENT A_VERSION _ -> throwError $ AGENT A_VERSION
joinConn c connId (CRContactUri (ConnReqUriData _ agentVRange (qUri :| _))) cInfo = joinConn c connId (CRContactUri (ConnReqUriData _ agentVRange (qUri :| _))) cInfo =
case ( qUri `compatibleVersion` SMP.smpClientVRange, case ( qUri `compatibleVersion` SMP.smpClientVRange,
@@ -269,7 +265,7 @@ joinConn c connId (CRContactUri (ConnReqUriData _ agentVRange (qUri :| _))) cInf
createReplyQueue :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> m () createReplyQueue :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
createReplyQueue c connId sq = do createReplyQueue c connId sq = do
srv <- getSMPServer c srv <- getSMPServer
(rq, qUri) <- newRcvQueue c srv (rq, qUri) <- newRcvQueue c srv
-- TODO reply queue version should be the same as send queue, ignoring it in v1 -- TODO reply queue version should be the same as send queue, ignoring it in v1
let qInfo = toVersionT qUri SMP.smpClientVersion let qInfo = toVersionT qUri SMP.smpClientVersion
@@ -316,6 +312,16 @@ subscribeConnection' c connId =
SomeConn _ (DuplexConnection _ rq sq) -> do SomeConn _ (DuplexConnection _ rq sq) -> do
resumeMsgDelivery c connId sq resumeMsgDelivery c connId sq
subscribeQueue c rq connId subscribeQueue c rq connId
case status (sq :: SndQueue) of
Confirmed -> do
-- TODO if there is no confirmation saved, just update the status without securing the queue
AcceptedConfirmation {senderConf = SMPConfirmation {senderKey}} <-
withStore (`getAcceptedConfirmation` connId)
secureQueue c rq senderKey
withStore $ \st -> setRcvQueueStatus st rq Secured
Secured -> pure ()
Active -> pure ()
_ -> throwError $ INTERNAL "unexpected queue status"
SomeConn _ (SndConnection _ sq) -> do SomeConn _ (SndConnection _ sq) -> do
resumeMsgDelivery c connId sq resumeMsgDelivery c connId sq
case status (sq :: SndQueue) of case status (sq :: SndQueue) of
@@ -348,12 +354,12 @@ enqueueMessage c connId sq aMessage = do
internalTs <- liftIO getCurrentTime internalTs <- liftIO getCurrentTime
(internalId, internalSndId, prevMsgHash) <- withStore (`updateSndIds` connId) (internalId, internalSndId, prevMsgHash) <- withStore (`updateSndIds` connId)
let privHeader = APrivHeader (unSndId internalSndId) prevMsgHash let privHeader = APrivHeader (unSndId internalSndId) prevMsgHash
agentMsg = AgentMessage privHeader aMessage agentMessage = smpEncode $ AgentMessage privHeader aMessage
agentMsgStr = smpEncode agentMsg internalHash = C.sha256Hash agentMessage
internalHash = C.sha256Hash agentMsgStr
encAgentMessage <- agentRatchetEncrypt connId agentMsgStr e2eEncUserMsgLength encAgentMessage <- agentRatchetEncrypt connId agentMessage e2eEncUserMsgLength
let msgBody = smpEncode $ AgentMsgEnvelope {agentVersion = smpAgentVersion, encAgentMessage} let msgBody = smpEncode $ AgentMsgEnvelope {agentVersion = smpAgentVersion, encAgentMessage}
msgType = agentMessageType agentMsg msgType = aMessageType aMessage
msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, internalHash, prevMsgHash} msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, internalHash, prevMsgHash}
withStore $ \st -> createSndMsg st connId msgData withStore $ \st -> createSndMsg st connId msgData
pure internalId pure internalId
@@ -363,13 +369,18 @@ resumeMsgDelivery c connId sq@SndQueue {server, sndId} = do
let qKey = (connId, server, sndId) let qKey = (connId, server, sndId)
unlessM (queueDelivering qKey) $ unlessM (queueDelivering qKey) $
async (runSmpQueueMsgDelivery c connId sq) async (runSmpQueueMsgDelivery c connId sq)
>>= \a -> atomically (TM.insert qKey a $ smpQueueMsgDeliveries c) >>= atomically . modifyTVar (smpQueueMsgDeliveries c) . M.insert qKey
unlessM connQueued $ unlessM connQueued $
withStore (`getPendingMsgs` connId) withStore (`getPendingMsgs` connId)
>>= queuePendingMsgs c connId sq >>= queuePendingMsgs c connId sq
where where
queueDelivering qKey = atomically $ isJust <$> TM.lookup qKey (smpQueueMsgDeliveries c) queueDelivering qKey = isJust . M.lookup qKey <$> readTVarIO (smpQueueMsgDeliveries c)
connQueued = atomically $ isJust <$> TM.lookupInsert connId True (connMsgsQueued c) connQueued =
atomically $
isJust
<$> stateTVar
(connMsgsQueued c)
(\m -> (M.lookup connId m, M.insert connId True m))
queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> [InternalId] -> m () queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> [InternalId] -> m ()
queuePendingMsgs c connId sq msgIds = atomically $ do queuePendingMsgs c connId sq msgIds = atomically $ do
@@ -379,11 +390,11 @@ queuePendingMsgs c connId sq msgIds = atomically $ do
getPendingMsgQ :: AgentClient -> ConnId -> SndQueue -> STM (TQueue InternalId) getPendingMsgQ :: AgentClient -> ConnId -> SndQueue -> STM (TQueue InternalId)
getPendingMsgQ c connId SndQueue {server, sndId} = do getPendingMsgQ c connId SndQueue {server, sndId} = do
let qKey = (connId, server, sndId) let qKey = (connId, server, sndId)
maybe (newMsgQueue qKey) pure =<< TM.lookup qKey (smpQueueMsgQueues c) maybe (newMsgQueue qKey) pure . M.lookup qKey =<< readTVar (smpQueueMsgQueues c)
where where
newMsgQueue qKey = do newMsgQueue qKey = do
mq <- newTQueue mq <- newTQueue
TM.insert qKey mq $ smpQueueMsgQueues c modifyTVar (smpQueueMsgQueues c) $ M.insert qKey mq
pure mq pure mq
runSmpQueueMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m () runSmpQueueMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
@@ -398,38 +409,30 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} connId sq = do
notify $ MERR mId (INTERNAL $ show e) notify $ MERR mId (INTERNAL $ show e)
Right (rq_, (msgType, msgBody, internalTs)) -> Right (rq_, (msgType, msgBody, internalTs)) ->
withRetryInterval ri $ \loop -> withRetryInterval ri $ \loop ->
tryError (send msgType c sq msgBody) >>= \case tryError (sendAgentMessage c sq msgBody) >>= \case
Left e -> do Left e -> do
let err = if msgType == AM_CONN_INFO then ERR e else MERR mId e
case e of case e of
SMP SMP.QUOTA -> case msgType of SMP SMP.QUOTA -> loop
AM_CONN_INFO -> connError msgId NOT_AVAILABLE
_ -> loop
SMP SMP.AUTH -> case msgType of SMP SMP.AUTH -> case msgType of
AM_CONN_INFO -> connError msgId NOT_AVAILABLE HELLO_ -> do
AM_HELLO_ -> do
helloTimeout <- asks $ helloTimeout . config helloTimeout <- asks $ helloTimeout . config
currentTime <- liftIO getCurrentTime currentTime <- liftIO getCurrentTime
if diffUTCTime currentTime internalTs > helloTimeout if diffUTCTime currentTime internalTs > helloTimeout
then case rq_ of then case rq_ of
-- party initiating connection -- party initiating connection
Just _ -> connError msgId NOT_AVAILABLE Just _ -> notifyDel msgId . ERR $ CONN NOT_AVAILABLE
-- party joining connection -- party joining connection
_ -> connError msgId NOT_ACCEPTED _ -> notifyDel msgId . ERR $ CONN NOT_ACCEPTED
else loop else loop
AM_REPLY_ -> notifyDel msgId $ ERR e REPLY_ -> notifyDel msgId $ ERR e
AM_A_MSG_ -> notifyDel msgId $ MERR mId e A_MSG_ -> notifyDel msgId $ MERR mId e
SMP (SMP.CMD _) -> notifyDel msgId err SMP (SMP.CMD _) -> notifyDel msgId $ MERR mId e
SMP SMP.LARGE_MSG -> notifyDel msgId err SMP SMP.LARGE_MSG -> notifyDel msgId $ MERR mId e
SMP {} -> notify err >> loop SMP {} -> notify (MERR mId e) >> loop
_ -> loop _ -> loop
Right () -> do Right () -> do
case msgType of case msgType of
AM_CONN_INFO -> do HELLO_ -> do
withStore $ \st -> setSndQueueStatus st sq Confirmed
when (isJust rq_) $ withStore (`removeConfirmations` connId)
void $ enqueueMessage c connId sq HELLO
AM_HELLO_ -> do
withStore $ \st -> setSndQueueStatus st sq Active withStore $ \st -> setSndQueueStatus st sq Active
case rq_ of case rq_ of
-- party initiating connection -- party initiating connection
@@ -438,20 +441,16 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} connId sq = do
notify CON notify CON
-- party joining connection -- party joining connection
_ -> createReplyQueue c connId sq _ -> createReplyQueue c connId sq
AM_A_MSG_ -> notify $ SENT mId A_MSG_ -> notify $ SENT mId
_ -> pure () _ -> pure ()
delMsg msgId delMsg msgId
where where
send = \case
AM_CONN_INFO -> sendConfirmation
_ -> sendAgentMessage
delMsg :: InternalId -> m () delMsg :: InternalId -> m ()
delMsg msgId = withStore $ \st -> deleteMsg st connId msgId delMsg msgId = withStore $ \st -> deleteMsg st connId msgId
notify :: ACommand 'Agent -> m () notify :: ACommand 'Agent -> m ()
notify cmd = atomically $ writeTBQueue subQ ("", connId, cmd) notify cmd = atomically $ writeTBQueue subQ ("", connId, cmd)
notifyDel :: InternalId -> ACommand 'Agent -> m () notifyDel :: InternalId -> ACommand 'Agent -> m ()
notifyDel msgId cmd = notify cmd >> delMsg msgId notifyDel msgId cmd = notify cmd >> delMsg msgId
connError msgId = notifyDel msgId . ERR . CONN
ackMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> AgentMsgId -> m () ackMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> AgentMsgId -> m ()
ackMessage' c connId msgId = do ackMessage' c connId msgId = do
@@ -487,18 +486,12 @@ deleteConnection' c connId =
delete :: RcvQueue -> m () delete :: RcvQueue -> m ()
delete rq = do delete rq = do
deleteQueue c rq deleteQueue c rq
atomically $ removeSubscription c connId removeSubscription c connId
withStore (`deleteConn` connId) withStore (`deleteConn` connId)
-- | Change servers to be used for creating new queues, in Reader monad getSMPServer :: AgentMonad m => m SMPServer
setSMPServers' :: forall m. AgentMonad m => AgentClient -> NonEmpty SMPServer -> m () getSMPServer =
setSMPServers' c servers = do asks (smpServers . config) >>= \case
atomically $ writeTVar (smpServers c) servers
getSMPServer :: AgentMonad m => AgentClient -> m SMPServer
getSMPServer c = do
smpServers <- readTVarIO $ smpServers c
case smpServers of
srv :| [] -> pure srv srv :| [] -> pure srv
servers -> do servers -> do
gen <- asks randomServer gen <- asks randomServer
@@ -513,7 +506,7 @@ subscriber c@AgentClient {msgQ} = forever $ do
Right _ -> return () Right _ -> return ()
processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SMPServerTransmission -> m () processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SMPServerTransmission -> m ()
processSMPTransmission c@AgentClient {smpClients, subQ} (srv, sessId, rId, cmd) = do processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do
withStore (\st -> getRcvConn st srv rId) >>= \case withStore (\st -> getRcvConn st srv rId) >>= \case
SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq
SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq
@@ -543,9 +536,8 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (srv, sessId, rId, cmd)
(SMP.PHEmpty, AgentMsgEnvelope _ encAgentMsg) -> do (SMP.PHEmpty, AgentMsgEnvelope _ encAgentMsg) -> do
agentMsgBody <- agentRatchetDecrypt connId encAgentMsg agentMsgBody <- agentRatchetDecrypt connId encAgentMsg
parseMessage agentMsgBody >>= \case parseMessage agentMsgBody >>= \case
agentMsg@(AgentMessage APrivHeader {sndMsgId, prevMsgHash} aMessage) -> do AgentMessage APrivHeader {sndMsgId, prevMsgHash} aMessage -> do
let msgType = agentMessageType agentMsg (msgId, msgMeta) <- agentClientMsg prevMsgHash sndMsgId (srvMsgId, systemToUTCTime srvTs) agentMsgBody aMessage
(msgId, msgMeta) <- agentClientMsg prevMsgHash sndMsgId (srvMsgId, systemToUTCTime srvTs) agentMsgBody msgType
case aMessage of case aMessage of
HELLO -> helloMsg >> ack >> withStore (\st -> deleteMsg st connId msgId) HELLO -> helloMsg >> ack >> withStore (\st -> deleteMsg st connId msgId)
REPLY cReq -> replyMsg cReq >> ack >> withStore (\st -> deleteMsg st connId msgId) REPLY cReq -> replyMsg cReq >> ack >> withStore (\st -> deleteMsg st connId msgId)
@@ -554,19 +546,10 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (srv, sessId, rId, cmd)
_ -> prohibited >> ack _ -> prohibited >> ack
_ -> prohibited >> ack _ -> prohibited >> ack
_ -> prohibited >> ack _ -> prohibited >> ack
SMP.END -> SMP.END -> do
atomically (TM.lookup srv smpClients >>= fmap join . mapM tryReadTMVar >>= processEND) removeSubscription c connId
>>= logServer "<--" c srv rId logServer "<--" c srv rId "END"
where notify END
processEND = \case
Just (Right clnt)
| sessId == sessionId clnt -> do
removeSubscription c connId
writeTBQueue subQ ("", connId, END)
pure "END"
| otherwise -> ignored
_ -> ignored
ignored = pure "END from disconnected client - ignored"
_ -> do _ -> do
logServer "<--" c srv rId $ "unexpected: " <> bshow cmd logServer "<--" c srv rId $ "unexpected: " <> bshow cmd
notify . ERR $ BROKER UNEXPECTED notify . ERR $ BROKER UNEXPECTED
@@ -644,13 +627,18 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (srv, sessId, rId, cmd)
case qInfo `proveCompatible` SMP.smpClientVRange of case qInfo `proveCompatible` SMP.smpClientVRange of
Nothing -> notify . ERR $ AGENT A_VERSION Nothing -> notify . ERR $ AGENT A_VERSION
Just qInfo' -> do Just qInfo' -> do
sq <- newSndQueue qInfo' (sq, smpConf) <- newSndQueue qInfo' ownConnInfo
liftIO $ print "before: upgradeRcvConnToDuplex st connId sq"
withStore $ \st -> upgradeRcvConnToDuplex st connId sq withStore $ \st -> upgradeRcvConnToDuplex st connId sq
enqueueConfirmation c connId sq ownConnInfo Nothing confirmQueue c connId sq smpConf Nothing
liftIO $ print "before: `removeConfirmations` connId"
withStore (`removeConfirmations` connId)
liftIO $ print "after: `removeConfirmations` connId"
void $ enqueueMessage c connId sq HELLO
_ -> prohibited _ -> prohibited
agentClientMsg :: PrevRcvMsgHash -> ExternalSndId -> (BrokerId, BrokerTs) -> MsgBody -> AgentMessageType -> m (InternalId, MsgMeta) agentClientMsg :: PrevRcvMsgHash -> ExternalSndId -> (BrokerId, BrokerTs) -> MsgBody -> AMessage -> m (InternalId, MsgMeta)
agentClientMsg externalPrevSndHash sndMsgId broker msgBody msgType = do agentClientMsg externalPrevSndHash sndMsgId broker msgBody aMessage = do
logServer "<--" c srv rId "MSG <MSG>" logServer "<--" c srv rId "MSG <MSG>"
let internalHash = C.sha256Hash msgBody let internalHash = C.sha256Hash msgBody
internalTs <- liftIO getCurrentTime internalTs <- liftIO getCurrentTime
@@ -658,6 +646,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (srv, sessId, rId, cmd)
let integrity = checkMsgIntegrity prevExtSndId sndMsgId prevRcvMsgHash externalPrevSndHash let integrity = checkMsgIntegrity prevExtSndId sndMsgId prevRcvMsgHash externalPrevSndHash
recipient = (unId internalId, internalTs) recipient = (unId internalId, internalTs)
msgMeta = MsgMeta {integrity, recipient, broker, sndMsgId} msgMeta = MsgMeta {integrity, recipient, broker, sndMsgId}
msgType = aMessageType aMessage
rcvMsg = RcvMsgData {msgMeta, msgType, msgBody, internalRcvId, internalHash, externalPrevSndHash} rcvMsg = RcvMsgData {msgMeta, msgType, msgBody, internalRcvId, internalHash, externalPrevSndHash}
withStore $ \st -> createRcvMsg st connId rcvMsg withStore $ \st -> createRcvMsg st connId rcvMsg
pure (internalId, msgMeta) pure (internalId, msgMeta)
@@ -682,9 +671,8 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (srv, sessId, rId, cmd)
| internalPrevMsgHash /= receivedPrevMsgHash = MsgError MsgBadHash | internalPrevMsgHash /= receivedPrevMsgHash = MsgError MsgBadHash
| otherwise = MsgError MsgDuplicate -- this case is not possible | otherwise = MsgError MsgDuplicate -- this case is not possible
confirmQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> m () confirmQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> SMPConfirmation -> Maybe (CR.E2ERatchetParams 'C.X448) -> m ()
confirmQueue c connId sq connInfo e2eEncryption = do confirmQueue c connId sq SMPConfirmation {senderKey, e2ePubKey, connInfo} e2eEncryption = do
_ <- withStore (`updateSndIds` connId)
msg <- mkConfirmation msg <- mkConfirmation
sendConfirmation c sq msg sendConfirmation c sq msg
withStore $ \st -> setSndQueueStatus st sq Confirmed withStore $ \st -> setSndQueueStatus st sq Confirmed
@@ -692,27 +680,9 @@ confirmQueue c connId sq connInfo e2eEncryption = do
mkConfirmation :: m MsgBody mkConfirmation :: m MsgBody
mkConfirmation = do mkConfirmation = do
encConnInfo <- agentRatchetEncrypt connId (smpEncode $ AgentConnInfo connInfo) e2eEncConnInfoLength encConnInfo <- agentRatchetEncrypt connId (smpEncode $ AgentConnInfo connInfo) e2eEncConnInfoLength
pure . smpEncode $ AgentConfirmation {agentVersion = smpAgentVersion, e2eEncryption, encConnInfo} let agentEnvelope = AgentConfirmation {agentVersion = smpAgentVersion, e2eEncryption, encConnInfo}
agentCbEncrypt sq (Just e2ePubKey) . smpEncode $
enqueueConfirmation :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> m () SMP.ClientMessage (SMP.PHConfirmation senderKey) $ smpEncode agentEnvelope
enqueueConfirmation c connId sq connInfo e2eEncryption = do
resumeMsgDelivery c connId sq
msgId <- storeConfirmation
queuePendingMsgs c connId sq [msgId]
where
storeConfirmation :: m InternalId
storeConfirmation = do
internalTs <- liftIO getCurrentTime
(internalId, internalSndId, prevMsgHash) <- withStore (`updateSndIds` connId)
let agentMsg = AgentConnInfo connInfo
agentMsgStr = smpEncode agentMsg
internalHash = C.sha256Hash agentMsgStr
encConnInfo <- agentRatchetEncrypt connId agentMsgStr e2eEncConnInfoLength
let msgBody = smpEncode $ AgentConfirmation {agentVersion = smpAgentVersion, e2eEncryption, encConnInfo}
msgType = agentMessageType agentMsg
msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, internalHash, prevMsgHash}
withStore $ \st -> createSndMsg st connId msgData
pure internalId
-- encoded AgentMessage -> encoded EncAgentMessage -- encoded AgentMessage -> encoded EncAgentMessage
agentRatchetEncrypt :: AgentMonad m => ConnId -> ByteString -> Int -> m ByteString agentRatchetEncrypt :: AgentMonad m => ConnId -> ByteString -> Int -> m ByteString
@@ -734,27 +704,27 @@ agentRatchetDecrypt connId encAgentMsg = do
notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m ()
notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON) notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON)
newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => Compatible SMPQueueInfo -> m SndQueue newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => Compatible SMPQueueInfo -> ConnInfo -> m (SndQueue, SMPConfirmation)
newSndQueue qInfo = newSndQueue qInfo cInfo =
asks (cmdSignAlg . config) >>= \case asks (cmdSignAlg . config) >>= \case
C.SignAlg a -> newSndQueue_ a qInfo C.SignAlg a -> newSndQueue_ a qInfo cInfo
newSndQueue_ :: newSndQueue_ ::
(C.SignatureAlgorithm a, C.AlgorithmI a, MonadUnliftIO m) => (C.SignatureAlgorithm a, C.AlgorithmI a, MonadUnliftIO m) =>
C.SAlgorithm a -> C.SAlgorithm a ->
Compatible SMPQueueInfo -> Compatible SMPQueueInfo ->
m SndQueue ConnInfo ->
newSndQueue_ a (Compatible (SMPQueueInfo _clientVersion smpServer senderId rcvE2ePubDhKey)) = do m (SndQueue, SMPConfirmation)
newSndQueue_ a (Compatible (SMPQueueInfo _clientVersion smpServer senderId rcvE2ePubDhKey)) cInfo = do
-- this function assumes clientVersion is compatible - it was tested before -- this function assumes clientVersion is compatible - it was tested before
(sndPublicKey, sndPrivateKey) <- liftIO $ C.generateSignatureKeyPair a (senderKey, sndPrivateKey) <- liftIO $ C.generateSignatureKeyPair a
(e2ePubKey, e2ePrivKey) <- liftIO C.generateKeyPair' (e2ePubKey, e2ePrivKey) <- liftIO C.generateKeyPair'
pure let sndQueue =
SndQueue SndQueue
{ server = smpServer, { server = smpServer,
sndId = senderId, sndId = senderId,
sndPublicKey = Just sndPublicKey, sndPrivateKey,
sndPrivateKey, e2eDhSecret = C.dh' rcvE2ePubDhKey e2ePrivKey,
e2eDhSecret = C.dh' rcvE2ePubDhKey e2ePrivKey, status = New
e2ePubKey = Just e2ePubKey, }
status = New pure (sndQueue, SMPConfirmation senderKey e2ePubKey cInfo)
}
+84 -142
View File
@@ -37,7 +37,7 @@ module Simplex.Messaging.Agent.Client
where where
import Control.Concurrent (forkIO) import Control.Concurrent (forkIO)
import Control.Concurrent.Async (Async, uninterruptibleCancel) import Control.Concurrent.Async (Async, async, uninterruptibleCancel)
import Control.Concurrent.STM (stateTVar) import Control.Concurrent.STM (stateTVar)
import Control.Logger.Simple import Control.Logger.Simple
import Control.Monad.Except import Control.Monad.Except
@@ -47,12 +47,13 @@ import Data.Bifunctor (first)
import Data.ByteString.Base64 import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (isNothing) import Data.Maybe (isNothing)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text.Encoding import Data.Text.Encoding
import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Env.Postgres
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store
@@ -61,12 +62,8 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Protocol (QueueId, QueueIdsKeys (..), SndPublicVerifyKey) import Simplex.Messaging.Protocol (QueueId, QueueIdsKeys (..), SndPublicVerifyKey)
import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Util (bshow, liftEitherError, liftError, liftIOEither, tryError)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (bshow, liftEitherError, liftError, tryError, whenM)
import Simplex.Messaging.Version import Simplex.Messaging.Version
import System.Timeout (timeout)
import UnliftIO (async, forConcurrently_)
import UnliftIO.Exception (Exception, IOException) import UnliftIO.Exception (Exception, IOException)
import qualified UnliftIO.Exception as E import qualified UnliftIO.Exception as E
import UnliftIO.STM import UnliftIO.STM
@@ -77,16 +74,13 @@ data AgentClient = AgentClient
{ rcvQ :: TBQueue (ATransmission 'Client), { rcvQ :: TBQueue (ATransmission 'Client),
subQ :: TBQueue (ATransmission 'Agent), subQ :: TBQueue (ATransmission 'Agent),
msgQ :: TBQueue SMPServerTransmission, msgQ :: TBQueue SMPServerTransmission,
smpServers :: TVar (NonEmpty SMPServer), smpClients :: TVar (Map SMPServer SMPClientVar),
smpClients :: TMap SMPServer SMPClientVar, subscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)),
subscrSrvrs :: TMap SMPServer (TMap ConnId RcvQueue), subscrConns :: TVar (Map ConnId SMPServer),
pendingSubscrSrvrs :: TMap SMPServer (TMap ConnId RcvQueue), connMsgsQueued :: TVar (Map ConnId Bool),
subscrConns :: TMap ConnId SMPServer, smpQueueMsgQueues :: TVar (Map (ConnId, SMPServer, SMP.SenderId) (TQueue InternalId)),
connMsgsQueued :: TMap ConnId Bool, smpQueueMsgDeliveries :: TVar (Map (ConnId, SMPServer, SMP.SenderId) (Async ())),
smpQueueMsgQueues :: TMap (ConnId, SMPServer, SMP.SenderId) (TQueue InternalId),
smpQueueMsgDeliveries :: TMap (ConnId, SMPServer, SMP.SenderId) (Async ()),
reconnections :: TVar [Async ()], reconnections :: TVar [Async ()],
asyncClients :: TVar [Async ()],
clientId :: Int, clientId :: Int,
agentEnv :: Env, agentEnv :: Env,
smpSubscriber :: Async (), smpSubscriber :: Async (),
@@ -99,19 +93,16 @@ newAgentClient agentEnv = do
rcvQ <- newTBQueue qSize rcvQ <- newTBQueue qSize
subQ <- newTBQueue qSize subQ <- newTBQueue qSize
msgQ <- newTBQueue qSize msgQ <- newTBQueue qSize
smpServers <- newTVar $ initialSMPServers (config agentEnv) smpClients <- newTVar M.empty
smpClients <- TM.empty subscrSrvrs <- newTVar M.empty
subscrSrvrs <- TM.empty subscrConns <- newTVar M.empty
pendingSubscrSrvrs <- TM.empty connMsgsQueued <- newTVar M.empty
subscrConns <- TM.empty smpQueueMsgQueues <- newTVar M.empty
connMsgsQueued <- TM.empty smpQueueMsgDeliveries <- newTVar M.empty
smpQueueMsgQueues <- TM.empty
smpQueueMsgDeliveries <- TM.empty
reconnections <- newTVar [] reconnections <- newTVar []
asyncClients <- newTVar []
clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1)
lock <- newTMVar () lock <- newTMVar ()
return AgentClient {rcvQ, subQ, msgQ, smpServers, smpClients, subscrSrvrs, pendingSubscrSrvrs, subscrConns, connMsgsQueued, smpQueueMsgQueues, smpQueueMsgDeliveries, reconnections, asyncClients, clientId, agentEnv, smpSubscriber = undefined, lock} return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, connMsgsQueued, smpQueueMsgQueues, smpQueueMsgDeliveries, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock}
-- | Agent monad with MonadReader Env and MonadError AgentErrorType -- | Agent monad with MonadReader Env and MonadError AgentErrorType
type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m)
@@ -133,48 +124,29 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv =
atomically getClientVar >>= either newSMPClient waitForSMPClient atomically getClientVar >>= either newSMPClient waitForSMPClient
where where
getClientVar :: STM (Either SMPClientVar SMPClientVar) getClientVar :: STM (Either SMPClientVar SMPClientVar)
getClientVar = maybe (Left <$> newClientVar) (pure . Right) =<< TM.lookup srv smpClients getClientVar = maybe (Left <$> newClientVar) (pure . Right) . M.lookup srv =<< readTVar smpClients
newClientVar :: STM SMPClientVar newClientVar :: STM SMPClientVar
newClientVar = do newClientVar = do
smpVar <- newEmptyTMVar smpVar <- newEmptyTMVar
TM.insert srv smpVar smpClients modifyTVar smpClients $ M.insert srv smpVar
pure smpVar pure smpVar
waitForSMPClient :: TMVar (Either AgentErrorType SMPClient) -> m SMPClient waitForSMPClient :: TMVar (Either AgentErrorType SMPClient) -> m SMPClient
waitForSMPClient smpVar = do waitForSMPClient = liftIOEither . atomically . readTMVar
SMPClientConfig {tcpTimeout} <- asks $ smpCfg . config
smpClient_ <- liftIO $ tcpTimeout `timeout` atomically (readTMVar smpVar)
liftEither $ case smpClient_ of
Just (Right smpClient) -> Right smpClient
Just (Left e) -> Left e
Nothing -> Left $ BROKER TIMEOUT
newSMPClient :: TMVar (Either AgentErrorType SMPClient) -> m SMPClient newSMPClient :: TMVar (Either AgentErrorType SMPClient) -> m SMPClient
newSMPClient smpVar = tryConnectClient pure tryConnectAsync newSMPClient smpVar =
where tryError connectClient >>= \r -> case r of
tryConnectClient :: (SMPClient -> m a) -> m () -> m a Right smp -> do
tryConnectClient successAction retryAction = logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
tryError connectClient >>= \r -> case r of atomically $ putTMVar smpVar r
Right smp -> do pure smp
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv Left e -> do
atomically $ putTMVar smpVar r atomically $ do
successAction smp putTMVar smpVar r
Left e -> do modifyTVar smpClients $ M.delete srv
if e == BROKER NETWORK || e == BROKER TIMEOUT throwError e
then retryAction
else atomically $ do
putTMVar smpVar (Left e)
TM.delete srv smpClients
throwError e
tryConnectAsync :: m ()
tryConnectAsync = do
a <- async connectAsync
atomically $ modifyTVar' (asyncClients c) (a :)
connectAsync :: m ()
connectAsync = do
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \loop -> void $ tryConnectClient (const reconnectClient) loop
connectClient :: m SMPClient connectClient :: m SMPClient
connectClient = do connectClient = do
@@ -188,62 +160,44 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv =
clientDisconnected :: UnliftIO m -> IO () clientDisconnected :: UnliftIO m -> IO ()
clientDisconnected u = do clientDisconnected u = do
removeClientAndSubs >>= (`forM_` serverDown u) removeClientSubs >>= (`forM_` serverDown u)
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
removeClientAndSubs :: IO (Maybe (Map ConnId RcvQueue)) removeClientSubs :: IO (Maybe (Map ConnId RcvQueue))
removeClientAndSubs = atomically $ do removeClientSubs = atomically $ do
TM.delete srv smpClients modifyTVar smpClients $ M.delete srv
cVar_ <- TM.lookupDelete srv $ subscrSrvrs c cs <- M.lookup srv <$> readTVar (subscrSrvrs c)
forM cVar_ $ \cVar -> do modifyTVar (subscrSrvrs c) $ M.delete srv
cs <- readTVar cVar modifyTVar (subscrConns c) $ maybe id (deleteKeys . M.keysSet) cs
modifyTVar' (subscrConns c) (`M.withoutKeys` M.keysSet cs) return cs
addPendingSubs cVar cs
pure cs
where where
addPendingSubs cVar cs = do deleteKeys :: Ord k => Set k -> Map k a -> Map k a
let ps = pendingSubscrSrvrs c deleteKeys ks m = S.foldr' M.delete m ks
TM.lookup srv ps >>= \case
Just v -> TM.union cs v
_ -> TM.insert srv cVar ps
serverDown :: UnliftIO m -> Map ConnId RcvQueue -> IO () serverDown :: UnliftIO m -> Map ConnId RcvQueue -> IO ()
serverDown u cs = unless (M.null cs) $ do serverDown u cs = unless (M.null cs) $ do
mapM_ (notifySub DOWN) $ M.keysSet cs mapM_ (notifySub DOWN) $ M.keysSet cs
unliftIO u reconnectServer a <- async . unliftIO u $ tryReconnectClient cs
atomically $ modifyTVar (reconnections c) (a :)
reconnectServer :: m () tryReconnectClient :: Map ConnId RcvQueue -> m ()
reconnectServer = do tryReconnectClient cs = do
a <- async tryReconnectClient
atomically $ modifyTVar' (reconnections c) (a :)
tryReconnectClient :: m ()
tryReconnectClient = do
ri <- asks $ reconnectInterval . config ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \loop -> withRetryInterval ri $ \loop ->
reconnectClient `catchError` const loop reconnectClient cs `catchError` const loop
reconnectClient :: m () reconnectClient :: Map ConnId RcvQueue -> m ()
reconnectClient = reconnectClient cs = do
withAgentLock c . withSMP c srv $ \smp -> do withAgentLock c . withSMP c srv $ \smp -> do
cs <- atomically $ mapM readTVar =<< TM.lookup srv (pendingSubscrSrvrs c) subs <- readTVarIO $ subscrConns c
forConcurrently_ (maybe [] M.toList cs) $ \sub@(connId, _) -> forM_ (M.toList cs) $ \(connId, rq@RcvQueue {rcvPrivateKey, rcvId}) ->
whenM (atomically $ isNothing <$> TM.lookup connId (subscrConns c)) $ when (isNothing $ M.lookup connId subs) $ do
subscribe_ smp sub `catchError` handleError connId subscribeSMPQueue smp rcvPrivateKey rcvId
where `catchError` \case
subscribe_ :: SMPClient -> (ConnId, RcvQueue) -> ExceptT SMPClientError IO () SMPServerError e -> liftIO $ notifySub (ERR $ SMP e) connId
subscribe_ smp (connId, rq@RcvQueue {rcvPrivateKey, rcvId}) = do e -> throwError e
subscribeSMPQueue smp rcvPrivateKey rcvId addSubscription c rq connId
addSubscription c rq connId liftIO $ notifySub UP connId
liftIO $ notifySub UP connId
handleError :: ConnId -> SMPClientError -> ExceptT SMPClientError IO ()
handleError connId = \case
e@SMPResponseTimeout -> throwError e
e@SMPNetworkError -> throwError e
e -> do
liftIO $ notifySub (ERR $ smpClientError e) connId
atomically $ removePendingSubscription c srv connId
notifySub :: ACommand 'Agent -> ConnId -> IO () notifySub :: ACommand 'Agent -> ConnId -> IO ()
notifySub cmd connId = atomically $ writeTBQueue (subQ c) ("", connId, cmd) notifySub cmd connId = atomically $ writeTBQueue (subQ c) ("", connId, cmd)
@@ -252,7 +206,6 @@ closeAgentClient :: MonadUnliftIO m => AgentClient -> m ()
closeAgentClient c = liftIO $ do closeAgentClient c = liftIO $ do
closeSMPServerClients c closeSMPServerClients c
cancelActions $ reconnections c cancelActions $ reconnections c
cancelActions $ asyncClients c
cancelActions $ smpQueueMsgDeliveries c cancelActions $ smpQueueMsgDeliveries c
closeSMPServerClients :: AgentClient -> IO () closeSMPServerClients :: AgentClient -> IO ()
@@ -341,41 +294,31 @@ newRcvQueue_ a c srv = do
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m () subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m ()
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do
atomically $ addPendingSubscription c rq connId withLogSMP c server rcvId "SUB" $ \smp ->
withLogSMP c server rcvId "SUB" $ \smp -> do subscribeSMPQueue smp rcvPrivateKey rcvId
liftIO (runExceptT $ subscribeSMPQueue smp rcvPrivateKey rcvId) >>= \case addSubscription c rq connId
Left e -> do
atomically . when (e /= SMPNetworkError && e /= SMPResponseTimeout) $
removePendingSubscription c server connId
throwError e
Right _ -> addSubscription c rq connId
addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m () addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m ()
addSubscription c rq@RcvQueue {server} connId = atomically $ do addSubscription c rq@RcvQueue {server} connId = atomically $ do
TM.insert connId server $ subscrConns c modifyTVar (subscrConns c) $ M.insert connId server
addSubs_ (subscrSrvrs c) rq connId modifyTVar (subscrSrvrs c) $ M.alter (Just . addSub) server
removePendingSubscription c server connId where
addSub :: Maybe (Map ConnId RcvQueue) -> Map ConnId RcvQueue
addSub (Just cs) = M.insert connId rq cs
addSub _ = M.singleton connId rq
addPendingSubscription :: AgentClient -> RcvQueue -> ConnId -> STM () removeSubscription :: AgentMonad m => AgentClient -> ConnId -> m ()
addPendingSubscription = addSubs_ . pendingSubscrSrvrs removeSubscription AgentClient {subscrConns, subscrSrvrs} connId = atomically $ do
cs <- readTVar subscrConns
addSubs_ :: TMap SMPServer (TMap ConnId RcvQueue) -> RcvQueue -> ConnId -> STM () writeTVar subscrConns $ M.delete connId cs
addSubs_ ss rq@RcvQueue {server} connId = mapM_
TM.lookup server ss >>= \case (modifyTVar subscrSrvrs . M.alter (>>= delSub))
Just m -> TM.insert connId rq m (M.lookup connId cs)
_ -> TM.singleton connId rq >>= \m -> TM.insert server m ss where
delSub :: Map ConnId RcvQueue -> Maybe (Map ConnId RcvQueue)
removeSubscription :: AgentClient -> ConnId -> STM () delSub cs =
removeSubscription c@AgentClient {subscrConns} connId = do let cs' = M.delete connId cs
server_ <- TM.lookupDelete connId subscrConns in if M.null cs' then Nothing else Just cs'
mapM_ (\server -> removeSubs_ (subscrSrvrs c) server connId) server_
removePendingSubscription :: AgentClient -> SMPServer -> ConnId -> STM ()
removePendingSubscription = removeSubs_ . pendingSubscrSrvrs
removeSubs_ :: TMap SMPServer (TMap ConnId RcvQueue) -> SMPServer -> ConnId -> STM ()
removeSubs_ ss server connId =
TM.lookup server ss >>= mapM_ (TM.delete connId)
logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m () logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m ()
logServer dir AgentClient {clientId} srv qId cmdStr = logServer dir AgentClient {clientId} srv qId cmdStr =
@@ -388,13 +331,11 @@ showServer SMPServer {host, port} =
logSecret :: ByteString -> ByteString logSecret :: ByteString -> ByteString
logSecret bs = encode $ B.take 3 bs logSecret bs = encode $ B.take 3 bs
-- TODO maybe package E2ERatchetParams into SMPConfirmation
sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m () sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
sendConfirmation c sq@SndQueue {server, sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = sendConfirmation c SndQueue {server, sndId} encConfirmation =
withLogSMP_ c server sndId "SEND <CONF>" $ \smp -> do withLogSMP_ c server sndId "SEND <CONF>" $ \smp ->
let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation liftSMP $ sendSMPMessage smp Nothing sndId encConfirmation
msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg
liftSMP $ sendSMPMessage smp Nothing sndId 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 :: forall m. AgentMonad m => AgentClient -> Compatible SMPQueueInfo -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> m ()
sendInvitation c (Compatible SMPQueueInfo {smpServer, senderId, dhPublicKey}) connReq connInfo = sendInvitation c (Compatible SMPQueueInfo {smpServer, senderId, dhPublicKey}) connReq connInfo =
@@ -429,6 +370,7 @@ deleteQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
withLogSMP c server rcvId "DEL" $ \smp -> withLogSMP c server rcvId "DEL" $ \smp ->
deleteSMPQueue smp rcvPrivateKey rcvId deleteSMPQueue smp rcvPrivateKey rcvId
-- TODO this is just wrong
sendAgentMessage :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m () sendAgentMessage :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} agentMsg = sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} agentMsg =
withLogSMP_ c server sndId "SEND <MSG>" $ \smp -> do withLogSMP_ c server sndId "SEND <MSG>" $ \smp -> do
@@ -0,0 +1,88 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module Simplex.Messaging.Agent.Env.Postgres
( AgentConfig (..),
defaultAgentConfig,
Env (..),
newSMPAgentEnv,
)
where
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.List.NonEmpty (NonEmpty)
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Database.PostgreSQL.Simple (ConnectInfo (..), defaultConnectInfo)
import Network.Socket
import Numeric.Natural
import Simplex.Messaging.Agent.Protocol (SMPServer)
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store.Postgres
import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import System.Random (StdGen, newStdGen)
import UnliftIO.STM
data AgentConfig = AgentConfig
{ tcpPort :: ServiceName,
smpServers :: NonEmpty SMPServer,
cmdSignAlg :: C.SignAlg,
connIdBytes :: Int,
tbqSize :: Natural,
dbConnInfo :: ConnectInfo,
dbPoolSize :: Int,
smpCfg :: SMPClientConfig,
reconnectInterval :: RetryInterval,
helloTimeout :: NominalDiffTime,
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath
}
defaultAgentConfig :: AgentConfig
defaultAgentConfig =
AgentConfig
{ tcpPort = "5224",
smpServers = undefined, -- TODO move it elsewhere?
cmdSignAlg = C.SignAlg C.SEd448,
connIdBytes = 12,
tbqSize = 16,
dbConnInfo = defaultConnectInfo {connectDatabase = "agent_poc_1"},
dbPoolSize = 4,
smpCfg = smpDefaultConfig,
reconnectInterval =
RetryInterval
{ initialInterval = second,
increaseAfter = 10 * second,
maxInterval = 10 * second
},
helloTimeout = 7 * nominalDay,
-- 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"
}
where
second = 1_000_000
data Env = Env
{ config :: AgentConfig,
store :: PostgresStore,
idsDrg :: TVar ChaChaDRG,
clientCounter :: TVar Int,
randomServer :: TVar StdGen
}
newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env
newSMPAgentEnv cfg@AgentConfig {dbConnInfo, dbPoolSize} = do
idsDrg <- newTVarIO =<< drgNew
store <- liftIO $ createPostgresStore dbConnInfo dbPoolSize Migrations.app
clientCounter <- newTVarIO 0
randomServer <- newTVarIO =<< liftIO newStdGen
return Env {config = cfg, store, idsDrg, clientCounter, randomServer}
+7 -9
View File
@@ -29,13 +29,12 @@ import UnliftIO.STM
data AgentConfig = AgentConfig data AgentConfig = AgentConfig
{ tcpPort :: ServiceName, { tcpPort :: ServiceName,
initialSMPServers :: NonEmpty SMPServer, smpServers :: NonEmpty SMPServer,
cmdSignAlg :: C.SignAlg, cmdSignAlg :: C.SignAlg,
connIdBytes :: Int, connIdBytes :: Int,
tbqSize :: Natural, tbqSize :: Natural,
dbFile :: FilePath, dbFile :: FilePath,
dbPoolSize :: Int, dbPoolSize :: Int,
yesToMigrations :: Bool,
smpCfg :: SMPClientConfig, smpCfg :: SMPClientConfig,
reconnectInterval :: RetryInterval, reconnectInterval :: RetryInterval,
helloTimeout :: NominalDiffTime, helloTimeout :: NominalDiffTime,
@@ -48,13 +47,12 @@ defaultAgentConfig :: AgentConfig
defaultAgentConfig = defaultAgentConfig =
AgentConfig AgentConfig
{ tcpPort = "5224", { tcpPort = "5224",
initialSMPServers = undefined, -- TODO move it elsewhere? smpServers = undefined, -- TODO move it elsewhere?
cmdSignAlg = C.SignAlg C.SEd448, cmdSignAlg = C.SignAlg C.SEd448,
connIdBytes = 12, connIdBytes = 12,
tbqSize = 64, tbqSize = 16,
dbFile = "smp-agent.db", dbFile = "smp-agent.db",
dbPoolSize = 4, dbPoolSize = 4,
yesToMigrations = False,
smpCfg = smpDefaultConfig, smpCfg = smpDefaultConfig,
reconnectInterval = reconnectInterval =
RetryInterval RetryInterval
@@ -62,7 +60,7 @@ defaultAgentConfig =
increaseAfter = 10 * second, increaseAfter = 10 * second,
maxInterval = 10 * second maxInterval = 10 * second
}, },
helloTimeout = 2 * nominalDay, helloTimeout = 7 * nominalDay,
-- CA certificate private key is not needed for initialization -- CA certificate private key is not needed for initialization
-- ! we do not generate these -- ! we do not generate these
caCertificateFile = "/etc/opt/simplex-agent/ca.crt", caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
@@ -81,9 +79,9 @@ data Env = Env
} }
newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env
newSMPAgentEnv config@AgentConfig {dbFile, dbPoolSize, yesToMigrations} = do newSMPAgentEnv cfg = do
idsDrg <- newTVarIO =<< drgNew idsDrg <- newTVarIO =<< drgNew
store <- liftIO $ createSQLiteStore dbFile dbPoolSize Migrations.app yesToMigrations store <- liftIO $ createSQLiteStore (dbFile cfg) (dbPoolSize cfg) Migrations.app
clientCounter <- newTVarIO 0 clientCounter <- newTVarIO 0
randomServer <- newTVarIO =<< liftIO newStdGen randomServer <- newTVarIO =<< liftIO newStdGen
return Env {config, store, idsDrg, clientCounter, randomServer} return Env {config = cfg, store, idsDrg, clientCounter, randomServer}
+9 -28
View File
@@ -46,9 +46,9 @@ module Simplex.Messaging.Agent.Protocol
SMPConfirmation (..), SMPConfirmation (..),
AgentMsgEnvelope (..), AgentMsgEnvelope (..),
AgentMessage (..), AgentMessage (..),
AgentMessageType (..),
APrivHeader (..), APrivHeader (..),
AMessage (..), AMessage (..),
AMsgType (..),
SMPServer (..), SMPServer (..),
SrvLoc (..), SrvLoc (..),
SMPQueueUri (..), SMPQueueUri (..),
@@ -89,7 +89,7 @@ module Simplex.Messaging.Agent.Protocol
connModeT, connModeT,
serializeQueueStatus, serializeQueueStatus,
queueStatusT, queueStatusT,
agentMessageType, aMessageType,
-- * TCP transport functions -- * TCP transport functions
tPut, tPut,
@@ -343,31 +343,6 @@ instance Encoding AgentMessage where
'M' -> AgentMessage <$> smpP <*> smpP 'M' -> AgentMessage <$> smpP <*> smpP
_ -> fail "bad AgentMessage" _ -> fail "bad AgentMessage"
data AgentMessageType = AM_CONN_INFO | AM_HELLO_ | AM_REPLY_ | AM_A_MSG_
deriving (Eq, Show)
instance Encoding AgentMessageType where
smpEncode = \case
AM_CONN_INFO -> "C"
AM_HELLO_ -> "H"
AM_REPLY_ -> "R"
AM_A_MSG_ -> "M"
smpP =
A.anyChar >>= \case
'C' -> pure AM_CONN_INFO
'H' -> pure AM_HELLO_
'R' -> pure AM_REPLY_
'M' -> pure AM_A_MSG_
_ -> fail "bad AgentMessageType"
agentMessageType :: AgentMessage -> AgentMessageType
agentMessageType = \case
AgentConnInfo _ -> AM_CONN_INFO
AgentMessage _ aMsg -> case aMsg of
HELLO -> AM_HELLO_
REPLY _ -> AM_REPLY_
A_MSG _ -> AM_A_MSG_
data APrivHeader = APrivHeader data APrivHeader = APrivHeader
{ -- | sequential ID assigned by the sending agent { -- | sequential ID assigned by the sending agent
sndMsgId :: AgentMsgId, sndMsgId :: AgentMsgId,
@@ -396,6 +371,12 @@ instance Encoding AMsgType where
'M' -> pure A_MSG_ 'M' -> pure A_MSG_
_ -> fail "bad AMsgType" _ -> fail "bad AMsgType"
aMessageType :: AMessage -> AMsgType
aMessageType = \case
HELLO -> HELLO_
REPLY _ -> REPLY_
A_MSG _ -> A_MSG_
-- | Messages sent between SMP agents once SMP queue is secured. -- | Messages sent between SMP agents once SMP queue is secured.
-- --
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#messages-between-smp-agents -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#messages-between-smp-agents
@@ -724,7 +705,7 @@ data ConnectionErrorType
SIMPLEX SIMPLEX
| -- | connection not accepted on join HELLO after timeout | -- | connection not accepted on join HELLO after timeout
NOT_ACCEPTED NOT_ACCEPTED
| -- | connection not available on reply confirmation/HELLO after timeout | -- | connection not available on reply HELLO after timeout
NOT_AVAILABLE NOT_AVAILABLE
deriving (Eq, Generic, Read, Show, Exception) deriving (Eq, Generic, Read, Show, Exception)
+1 -1
View File
@@ -19,7 +19,7 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
import Simplex.Messaging.Agent import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Env.Postgres
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), simplexMQVersion) import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), simplexMQVersion)
import Simplex.Messaging.Transport.Server (loadTLSServerParams, runTransportServer) import Simplex.Messaging.Transport.Server (loadTLSServerParams, runTransportServer)
+4 -7
View File
@@ -62,7 +62,7 @@ class Monad m => MonadAgentStore s m where
createRcvMsg :: s -> ConnId -> RcvMsgData -> m () createRcvMsg :: s -> ConnId -> RcvMsgData -> m ()
updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash)
createSndMsg :: s -> ConnId -> SndMsgData -> m () createSndMsg :: s -> ConnId -> SndMsgData -> m ()
getPendingMsgData :: s -> ConnId -> InternalId -> m (Maybe RcvQueue, (AgentMessageType, MsgBody, InternalTs)) getPendingMsgData :: s -> ConnId -> InternalId -> m (Maybe RcvQueue, (AMsgType, MsgBody, InternalTs))
getPendingMsgs :: s -> ConnId -> m [InternalId] getPendingMsgs :: s -> ConnId -> m [InternalId]
checkRcvMsg :: s -> ConnId -> InternalId -> m () checkRcvMsg :: s -> ConnId -> InternalId -> m ()
deleteMsg :: s -> ConnId -> InternalId -> m () deleteMsg :: s -> ConnId -> InternalId -> m ()
@@ -102,11 +102,8 @@ data SndQueue = SndQueue
{ server :: SMPServer, { server :: SMPServer,
-- | sender queue ID -- | sender queue ID
sndId :: SMP.SenderId, sndId :: SMP.SenderId,
-- | key pair used by the sender to sign transmissions -- | key used by the sender to sign transmissions
sndPublicKey :: Maybe C.APublicVerifyKey,
sndPrivateKey :: SndPrivateSignKey, sndPrivateKey :: SndPrivateSignKey,
-- | DH public key used to negotiate per-queue e2e encryption
e2ePubKey :: Maybe C.PublicKeyX25519,
-- | shared DH secret agreed for simple per-queue e2e encryption -- | shared DH secret agreed for simple per-queue e2e encryption
e2eDhSecret :: C.DhSecretX25519, e2eDhSecret :: C.DhSecretX25519,
-- | queue status -- | queue status
@@ -224,7 +221,7 @@ type PrevSndMsgHash = MsgHash
data RcvMsgData = RcvMsgData data RcvMsgData = RcvMsgData
{ msgMeta :: MsgMeta, { msgMeta :: MsgMeta,
msgType :: AgentMessageType, msgType :: AMsgType,
msgBody :: MsgBody, msgBody :: MsgBody,
internalRcvId :: InternalRcvId, internalRcvId :: InternalRcvId,
internalHash :: MsgHash, internalHash :: MsgHash,
@@ -235,7 +232,7 @@ data SndMsgData = SndMsgData
{ internalId :: InternalId, { internalId :: InternalId,
internalSndId :: InternalSndId, internalSndId :: InternalSndId,
internalTs :: InternalTs, internalTs :: InternalTs,
msgType :: AgentMessageType, msgType :: AMsgType,
msgBody :: MsgBody, msgBody :: MsgBody,
internalHash :: MsgHash, internalHash :: MsgHash,
prevMsgHash :: MsgHash prevMsgHash :: MsgHash
@@ -0,0 +1,957 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Simplex.Messaging.Agent.Store.Postgres
( PostgresStore (..),
createPostgresStore,
connectPostgresStore,
withConnection,
withTransaction,
fromTextField_,
firstRow,
)
where
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM
import Control.Exception (bracket)
import Control.Monad (void)
import Control.Monad.Except
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
import Data.Bifunctor (second)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Base64.URL as U
import qualified Data.ByteString.Char8 as B
import Data.Char (toLower)
import Data.Functor (($>))
import Data.List (find, foldl')
import qualified Data.Map.Strict as M
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1)
import Database.PostgreSQL.Simple (FromRow, Only (..), Query, SqlError, ToRow, withSavepoint)
import qualified Database.PostgreSQL.Simple as DB
import Database.PostgreSQL.Simple.Errors (constraintViolation)
import Database.PostgreSQL.Simple.FromField
import Database.PostgreSQL.Simple.Internal (Conversion (..), Field (..))
import Database.PostgreSQL.Simple.SqlQQ (sql)
import Database.PostgreSQL.Simple.ToField (ToField (..))
import qualified Database.PostgreSQL.Simple.TypeInfo
import Database.PostgreSQL.Simple.TypeInfo.Static (bytea, text)
import qualified Database.PostgreSQL.Simple.TypeInfo.Static
import GHC.Word (Word32)
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.Postgres.Migrations (Migration)
import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (blobFieldParser, parseAll)
import Simplex.Messaging.Protocol (MsgBody)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Util (bshow, liftIOEither)
import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist)
import System.Exit (exitFailure)
import System.FilePath (takeDirectory)
import System.IO (hFlush, stdout)
import qualified UnliftIO.Exception as E
import Network.Socket (HostName, ServiceName)
import Simplex.Messaging.Crypto (KeyHash)
-- * Postgres Store implementation
data PostgresStore = PostgresStore
{ dbConnInfo :: DB.ConnectInfo,
dbConnPool :: TBQueue DB.Connection,
dbNew :: Bool
}
createPostgresStore :: DB.ConnectInfo -> Int -> [Migration] -> IO PostgresStore
createPostgresStore dbConnInfo poolSize migrations = do
st <- connectPostgresStore dbConnInfo poolSize
migrateSchema st migrations
pure st
migrateSchema :: PostgresStore -> [Migration] -> IO ()
migrateSchema st migrations = withConnection st $ \db -> do
Migrations.initialize db
Migrations.get db migrations >>= \case
Left e -> confirmOrExit $ "Database error: " <> e
Right [] -> pure ()
Right ms -> do
unless (dbNew st) $ do
confirmOrExit "The app has a newer version than the database - it will be backed up and upgraded."
-- TODO backup
-- let f = dbFilePath st
-- copyFile f (f <> ".bak")
Migrations.run db ms
confirmOrExit :: String -> IO ()
confirmOrExit s = do
putStrLn s
putStr "Continue (y/N): "
hFlush stdout
ok <- getLine
when (map toLower ok /= "y") exitFailure
connectPostgresStore :: DB.ConnectInfo -> Int -> IO PostgresStore
connectPostgresStore dbConnInfo poolSize = do
let dbNew = True -- TODO scan migrations
dbConnPool <- newTBQueueIO $ toEnum poolSize
replicateM_ poolSize $
connectDB dbConnInfo >>= atomically . writeTBQueue dbConnPool
pure PostgresStore {dbConnInfo, dbConnPool, dbNew}
connectDB :: DB.ConnectInfo -> IO DB.Connection
connectDB = DB.connect
checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a)
checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err)
handleSQLError :: StoreError -> SqlError -> StoreError
handleSQLError err e = case constraintViolation e of
Just _ -> err
Nothing -> SEInternal $ bshow e
withConnection :: PostgresStore -> (DB.Connection -> IO a) -> IO a
withConnection PostgresStore {dbConnPool} =
bracket
(atomically $ readTBQueue dbConnPool)
(atomically . writeTBQueue dbConnPool)
execute :: ToRow q => DB.Connection -> Query -> q -> IO ()
execute db query q = void $ DB.execute db query q
-- TODO not sure this logic is needed with Postgres, also no such error
-- withTransaction :: forall a. PostgresStore -> (DB.Connection -> IO a) -> IO a
-- withTransaction st action = withConnection st $ loop 100 100_000
-- where
-- loop :: Int -> Int -> DB.Connection -> IO a
-- loop t tLim db =
-- DB.withTransaction db (action db) `E.catch` \(e :: SQLError) ->
-- if tLim > t && DB.sqlError e == DB.ErrorBusy
-- then do
-- threadDelay t
-- loop (t * 9 `div` 8) (tLim - t) db
-- else E.throwIO e
withTransaction :: forall a. PostgresStore -> (DB.Connection -> IO a) -> IO a
withTransaction st action = withConnection st inTransaction
where
inTransaction :: DB.Connection -> IO a
inTransaction db = DB.withTransaction db (action db)
createConn_ ::
(MonadUnliftIO m, MonadError StoreError m) =>
PostgresStore ->
TVar ChaChaDRG ->
ConnData ->
(DB.Connection -> ByteString -> IO ()) ->
m ByteString
createConn_ st gVar cData create = do
connId <- liftIOEither . checkConstraint SEConnDuplicate . withTransaction st $ \db ->
case cData of
ConnData {connId = ""} -> createWithRandomId gVar $ create db
ConnData {connId} -> create db connId $> Right connId
liftIO $ print "before: getConn_ db connId"
conn <- liftIO $ withTransaction st $ \db -> getConn_ db connId
liftIO $ print conn
pure connId
instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore PostgresStore m where
createRcvConn :: PostgresStore -> TVar ChaChaDRG -> ConnData -> RcvQueue -> SConnectionMode c -> m ConnId
createRcvConn st gVar cData q@RcvQueue {server} cMode =
createConn_ st gVar cData $ \db connId -> do
upsertServer_ db server
execute db "INSERT INTO connections (conn_id, conn_mode) VALUES (?, ?)" (connId, cMode)
insertRcvQueue_ db connId q
createSndConn :: PostgresStore -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId
createSndConn st gVar cData q@SndQueue {server} =
createConn_ st gVar cData $ \db connId -> do
upsertServer_ db server
execute db "INSERT INTO connections (conn_id, conn_mode) VALUES (?, ?)" (connId, SCMInvitation)
insertSndQueue_ db connId q
getConn :: PostgresStore -> ConnId -> m SomeConn
getConn st connId =
liftIOEither . withTransaction st $ \db ->
getConn_ db connId
getRcvConn :: PostgresStore -> SMPServer -> SMP.RecipientId -> m SomeConn
getRcvConn st SMPServer {host, port} rcvId =
liftIOEither . withTransaction st $ \db ->
DB.query
db
[sql|
SELECT q.conn_id
FROM rcv_queues q
WHERE q.host = ? AND q.port = ? AND q.rcv_id = ?;
|]
(host, port, rcvId)
>>= \case
[Only connId] -> getConn_ db connId
_ -> pure $ Left SEConnNotFound
deleteConn :: PostgresStore -> ConnId -> m ()
deleteConn st connId =
liftIO . withTransaction st $ \db ->
execute
db
"DELETE FROM connections WHERE conn_id = ?;"
(Only connId)
upgradeRcvConnToDuplex :: PostgresStore -> ConnId -> SndQueue -> m ()
upgradeRcvConnToDuplex st connId sq@SndQueue {server} =
liftIOEither . withTransaction st $ \db ->
getConn_ db connId >>= \case
Right (SomeConn _ RcvConnection {}) -> do
upsertServer_ db server
insertSndQueue_ db connId sq
pure $ Right ()
Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c
_ -> pure $ Left SEConnNotFound
upgradeSndConnToDuplex :: PostgresStore -> ConnId -> RcvQueue -> m ()
upgradeSndConnToDuplex st connId rq@RcvQueue {server} =
liftIOEither . withTransaction st $ \db ->
getConn_ db connId >>= \case
Right (SomeConn _ SndConnection {}) -> do
upsertServer_ db server
insertRcvQueue_ db connId rq
pure $ Right ()
Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c
_ -> pure $ Left SEConnNotFound
setRcvQueueStatus :: PostgresStore -> RcvQueue -> QueueStatus -> m ()
setRcvQueueStatus st RcvQueue {rcvId, server = SMPServer {host, port}} status =
-- ? throw error if queue does not exist?
liftIO . withTransaction st $ \db ->
execute
db
[sql|
UPDATE rcv_queues
SET status = ?
WHERE host = ? AND port = ? AND rcv_id = ?;
|]
(status, host, port, rcvId)
setRcvQueueConfirmedE2E :: PostgresStore -> RcvQueue -> C.DhSecretX25519 -> m ()
setRcvQueueConfirmedE2E st RcvQueue {rcvId, server = SMPServer {host, port}} e2eDhSecret =
liftIO . withTransaction st $ \db ->
execute
db
[sql|
UPDATE rcv_queues
SET e2e_dh_secret = ?,
status = ?
WHERE host = ? AND port = ? AND rcv_id = ?
|]
(Confirmed, e2eDhSecret, host, port, rcvId)
setSndQueueStatus :: PostgresStore -> SndQueue -> QueueStatus -> m ()
setSndQueueStatus st SndQueue {sndId, server = SMPServer {host, port}} status =
-- ? throw error if queue does not exist?
liftIO . withTransaction st $ \db ->
execute
db
[sql|
UPDATE snd_queues
SET status = ?
WHERE host = ? AND port = ? AND snd_id = ?;
|]
(status, host, port, sndId)
createConfirmation :: PostgresStore -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId
createConfirmation st gVar NewConfirmation {connId, senderConf = SMPConfirmation {senderKey, e2ePubKey, connInfo}, ratchetState} =
liftIOEither . withTransaction st $ \db ->
createWithRandomId gVar $ \confirmationId ->
execute
db
[sql|
INSERT INTO conn_confirmations
(confirmation_id, conn_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info, accepted) VALUES (?, ?, ?, ?, ?, ?, 0);
|]
(confirmationId, connId, senderKey, e2ePubKey, ratchetState, connInfo)
acceptConfirmation :: PostgresStore -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation
acceptConfirmation st confirmationId ownConnInfo =
liftIOEither . withTransaction st $ \db -> do
execute
db
[sql|
UPDATE conn_confirmations
SET accepted = 1,
own_conn_info = ?
WHERE confirmation_id = ?;
|]
(ownConnInfo, confirmationId)
firstRow confirmation SEConfirmationNotFound $
DB.query
db
[sql|
SELECT conn_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info
FROM conn_confirmations
WHERE confirmation_id = ?;
|]
(Only confirmationId)
where
confirmation (connId, senderKey, e2ePubKey, ratchetState, connInfo) =
AcceptedConfirmation
{ confirmationId,
connId,
senderConf = SMPConfirmation {senderKey, e2ePubKey, connInfo},
ratchetState,
ownConnInfo
}
getAcceptedConfirmation :: PostgresStore -> ConnId -> m AcceptedConfirmation
getAcceptedConfirmation st connId =
liftIOEither . withTransaction st $ \db ->
firstRow confirmation SEConfirmationNotFound $
DB.query
db
[sql|
SELECT confirmation_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info, own_conn_info
FROM conn_confirmations
WHERE conn_id = ? AND accepted = 1;
|]
(Only connId)
where
confirmation (confirmationId, senderKey, e2ePubKey, ratchetState, connInfo, ownConnInfo) =
AcceptedConfirmation
{ confirmationId,
connId,
senderConf = SMPConfirmation {senderKey, e2ePubKey, connInfo},
ratchetState,
ownConnInfo
}
removeConfirmations :: PostgresStore -> ConnId -> m ()
removeConfirmations st connId =
liftIO . withTransaction st $ \db ->
execute
db
[sql|
DELETE FROM conn_confirmations
WHERE conn_id = ?;
|]
(Only connId)
createInvitation :: PostgresStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId
createInvitation st gVar NewInvitation {contactConnId, connReq, recipientConnInfo} =
liftIOEither . withTransaction st $ \db ->
createWithRandomId gVar $ \invitationId ->
execute
db
[sql|
INSERT INTO conn_invitations
(invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0);
|]
(invitationId, contactConnId, connReq, recipientConnInfo)
getInvitation :: PostgresStore -> InvitationId -> m Invitation
getInvitation st invitationId =
liftIOEither . withTransaction st $ \db ->
firstRow invitation SEInvitationNotFound $
DB.query
db
[sql|
SELECT contact_conn_id, cr_invitation, recipient_conn_info, own_conn_info, accepted
FROM conn_invitations
WHERE invitation_id = ?
AND accepted = 0
|]
(Only invitationId)
where
invitation (contactConnId, connReq, recipientConnInfo, ownConnInfo, accepted) =
Invitation {invitationId, contactConnId, connReq, recipientConnInfo, ownConnInfo, accepted}
acceptInvitation :: PostgresStore -> InvitationId -> ConnInfo -> m ()
acceptInvitation st invitationId ownConnInfo =
liftIO . withTransaction st $ \db -> do
execute
db
[sql|
UPDATE conn_invitations
SET accepted = 1,
own_conn_info = ?
WHERE invitation_id = ?
|]
(ownConnInfo, invitationId)
deleteInvitation :: PostgresStore -> ConnId -> InvitationId -> m ()
deleteInvitation st contactConnId invId =
liftIOEither . withTransaction st $ \db ->
runExceptT $
ExceptT (getConn_ db contactConnId) >>= \case
SomeConn SCContact _ ->
liftIO $ execute db "DELETE FROM conn_invitations WHERE contact_conn_id = ? AND invitation_id = ?" (contactConnId, invId)
_ -> throwError SEConnNotFound
updateRcvIds :: PostgresStore -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
updateRcvIds st connId =
liftIO . withTransaction st $ \db -> do
(lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ db connId
let internalId = InternalId $ unId lastInternalId + 1
internalRcvId = InternalRcvId $ unRcvId lastInternalRcvId + 1
updateLastIdsRcv_ db connId internalId internalRcvId
pure (internalId, internalRcvId, lastExternalSndId, lastRcvHash)
createRcvMsg :: PostgresStore -> ConnId -> RcvMsgData -> m ()
createRcvMsg st connId rcvMsgData =
liftIO . withTransaction st $ \db -> do
insertRcvMsgBase_ db connId rcvMsgData
insertRcvMsgDetails_ db connId rcvMsgData
updateHashRcv_ db connId rcvMsgData
updateSndIds :: PostgresStore -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash)
updateSndIds st connId =
liftIO . withTransaction st $ \db -> do
(lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ db connId
let internalId = InternalId $ unId lastInternalId + 1
internalSndId = InternalSndId $ unSndId lastInternalSndId + 1
updateLastIdsSnd_ db connId internalId internalSndId
pure (internalId, internalSndId, prevSndHash)
createSndMsg :: PostgresStore -> ConnId -> SndMsgData -> m ()
createSndMsg st connId sndMsgData =
liftIO . withTransaction st $ \db -> do
insertSndMsgBase_ db connId sndMsgData
insertSndMsgDetails_ db connId sndMsgData
updateHashSnd_ db connId sndMsgData
getPendingMsgData :: PostgresStore -> ConnId -> InternalId -> m (Maybe RcvQueue, (AMsgType, MsgBody, InternalTs))
getPendingMsgData st connId msgId =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
rq_ <- liftIO $ getRcvQueueByConnId_ db connId
msgData <-
ExceptT . firstRow id SEMsgNotFound $
DB.query
db
[sql|
SELECT m.msg_type, m.msg_body, m.internal_ts
FROM messages m
JOIN snd_messages s ON s.conn_id = m.conn_id AND s.internal_id = m.internal_id
WHERE m.conn_id = ? AND m.internal_id = ?
|]
(connId, msgId)
pure (rq_, msgData)
getPendingMsgs :: PostgresStore -> ConnId -> m [InternalId]
getPendingMsgs st connId =
liftIO . withTransaction st $ \db ->
map fromOnly
<$> DB.query db "SELECT internal_id FROM snd_messages WHERE conn_id = ?" (Only connId)
checkRcvMsg :: PostgresStore -> ConnId -> InternalId -> m ()
checkRcvMsg st connId msgId =
liftIOEither . withTransaction st $ \db ->
hasMsg
<$> DB.query
db
[sql|
SELECT conn_id, internal_id
FROM rcv_messages
WHERE conn_id = ? AND internal_id = ?
|]
(connId, msgId)
where
hasMsg :: [(ConnId, InternalId)] -> Either StoreError ()
hasMsg r = if null r then Left SEMsgNotFound else Right ()
deleteMsg :: PostgresStore -> ConnId -> InternalId -> m ()
deleteMsg st connId msgId =
liftIO . withTransaction st $ \db ->
execute db "DELETE FROM messages WHERE conn_id = ? AND internal_id = ?;" (connId, msgId)
createRatchetX3dhKeys :: PostgresStore -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> m ()
createRatchetX3dhKeys st connId x3dhPrivKey1 x3dhPrivKey2 =
liftIO . withTransaction st $ \db ->
execute db "INSERT INTO ratchets (conn_id, x3dh_priv_key_1, x3dh_priv_key_2) VALUES (?, ?, ?)" (connId, x3dhPrivKey1, x3dhPrivKey2)
getRatchetX3dhKeys :: PostgresStore -> ConnId -> m (C.PrivateKeyX448, C.PrivateKeyX448)
getRatchetX3dhKeys st connId =
liftIOEither . withTransaction st $ \db ->
fmap hasKeys $
firstRow id SEX3dhKeysNotFound $
DB.query db "SELECT x3dh_priv_key_1, x3dh_priv_key_2 FROM ratchets WHERE conn_id = ?" (Only connId)
where
hasKeys = \case
Right (Just k1, Just k2) -> Right (k1, k2)
_ -> Left SEX3dhKeysNotFound
createRatchet :: PostgresStore -> ConnId -> RatchetX448 -> m ()
createRatchet st connId rc =
liftIO . withTransaction st $ \db -> do
execute
db
[sql|
INSERT INTO ratchets (conn_id, ratchet_state)
VALUES (?, ?)
ON CONFLICT (conn_id) DO UPDATE SET
ratchet_state = ?,
x3dh_priv_key_1 = NULL,
x3dh_priv_key_2 = NULL
|]
(connId, rc, rc)
getRatchet :: PostgresStore -> ConnId -> m RatchetX448
getRatchet st connId =
liftIOEither . withTransaction st $ \db ->
ratchet
<$> DB.query db "SELECT ratchet_state FROM ratchets WHERE conn_id = ?" (Only connId)
where
ratchet (Only (Just rc) : _) = Right rc
ratchet _ = Left SERatchetNotFound
getSkippedMsgKeys :: PostgresStore -> ConnId -> m SkippedMsgKeys
getSkippedMsgKeys st connId =
liftIO . withTransaction st $ \db ->
skipped <$> DB.query db "SELECT header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ?" (Only connId)
where
skipped ms = foldl' addSkippedKey M.empty ms
addSkippedKey smks (hk, msgN, mk) = M.alter (Just . addMsgKey) hk smks
where
addMsgKey = maybe (M.singleton msgN mk) (M.insert msgN mk)
updateRatchet :: PostgresStore -> ConnId -> RatchetX448 -> SkippedMsgDiff -> m ()
updateRatchet st connId rc skipped =
liftIO . withTransaction st $ \db -> do
execute db "UPDATE ratchets SET ratchet_state = ? WHERE conn_id = ?" (rc, connId)
case skipped of
SMDNoChange -> pure ()
SMDRemove hk msgN ->
execute db "DELETE FROM skipped_messages WHERE conn_id = ? AND header_key = ? AND msg_n = ?" (connId, hk, msgN)
SMDAdd smks ->
forM_ (M.assocs smks) $ \(hk, mks) ->
forM_ (M.assocs mks) $ \(msgN, mk) ->
execute db "INSERT INTO skipped_messages (conn_id, header_key, msg_n, msg_key) VALUES (?, ?, ?, ?)" (connId, hk, msgN, mk)
-- -- * Auxiliary helpers
instance ToField QueueStatus where toField = toField . serializeQueueStatus
instance FromField QueueStatus where fromField = fromTextField_ queueStatusT
instance ToField InternalRcvId where toField (InternalRcvId x) = toField x
instance FromField InternalRcvId where fromField x = fromField x
instance ToField InternalSndId where toField (InternalSndId x) = toField x
instance FromField InternalSndId where fromField x = fromField x
instance ToField InternalId where toField (InternalId x) = toField x
instance FromField InternalId where fromField x = fromField x
instance ToField AMsgType where toField = toField . smpEncode
instance FromField AMsgType where fromField = fromByteStringField $ parseAll smpP
instance ToField MsgIntegrity where toField = toField . strEncode
instance FromField MsgIntegrity where fromField = fromByteStringField $ parseAll strP
instance ToField SMPQueueUri where toField = toField . strEncode
instance FromField SMPQueueUri where fromField = fromByteStringField $ parseAll strP
instance ToField AConnectionRequestUri where toField = toField . strEncode
instance FromField AConnectionRequestUri where fromField = fromByteStringField $ parseAll strP
instance ConnectionModeI c => ToField (ConnectionRequestUri c) where toField = toField . strEncode
instance (E.Typeable c, ConnectionModeI c) => FromField (ConnectionRequestUri c) where fromField = fromByteStringField $ parseAll strP
instance ToField ConnectionMode where toField = toField . decodeLatin1 . strEncode
instance FromField ConnectionMode where fromField = fromTextField_ connModeT
instance ToField (SConnectionMode c) where toField = toField . connMode
instance FromField AConnectionMode where fromField = fromTextField_ $ fmap connMode' . connModeT
instance FromField Word32 where fromField x = fromField x
fromTextField_ :: E.Typeable a => (Text -> Maybe a) -> Field -> Maybe ByteString -> Conversion a
fromTextField_ fromText f mdata =
if typeOid f /= typoid text
then returnError Incompatible f ""
else case mdata of
Nothing -> returnError UnexpectedNull f ""
Just dat ->
case fromText ((T.pack . B.unpack) dat) of
Just x -> return x
_ -> returnError ConversionFailed f (B.unpack dat)
-- TODO same as in Crypto
fromByteStringField :: E.Typeable a => (ByteString -> Either String a) -> Field -> Maybe ByteString -> Conversion a
fromByteStringField dec f mdata =
if typeOid f /= typoid bytea
then returnError Incompatible f ""
else case mdata of
Nothing -> returnError UnexpectedNull f ""
Just dat ->
case dec dat of
Right x -> return x
_ -> returnError ConversionFailed f (B.unpack dat)
listToEither :: e -> [a] -> Either e a
listToEither _ (x : _) = Right x
listToEither e _ = Left e
firstRow :: (a -> b) -> e -> IO [a] -> IO (Either e b)
firstRow f e a = second f . listToEither e <$> a
-- {- ORMOLU_DISABLE -}
-- -- SQLite.Simple only has these up to 10 fields, which is insufficient for some of our queries
-- instance (FromField a, FromField b, FromField c, FromField d, FromField e,
-- FromField f, FromField g, FromField h, FromField i, FromField j,
-- FromField k) =>
-- FromRow (a,b,c,d,e,f,g,h,i,j,k) where
-- fromRow = (,,,,,,,,,,) <$> field <*> field <*> field <*> field <*> field
-- <*> field <*> field <*> field <*> field <*> field
-- <*> field
-- instance (FromField a, FromField b, FromField c, FromField d, FromField e,
-- FromField f, FromField g, FromField h, FromField i, FromField j,
-- FromField k, FromField l) =>
-- FromRow (a,b,c,d,e,f,g,h,i,j,k,l) where
-- fromRow = (,,,,,,,,,,,) <$> field <*> field <*> field <*> field <*> field
-- <*> field <*> field <*> field <*> field <*> field
-- <*> field <*> field
-- instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f,
-- ToField g, ToField h, ToField i, ToField j, ToField k, ToField l) =>
-- ToRow (a,b,c,d,e,f,g,h,i,j,k,l) where
-- toRow (a,b,c,d,e,f,g,h,i,j,k,l) =
-- [ toField a, toField b, toField c, toField d, toField e, toField f,
-- toField g, toField h, toField i, toField j, toField k, toField l
-- ]
-- {- ORMOLU_ENABLE -}
-- * Server upsert helper
upsertServer_ :: DB.Connection -> SMPServer -> IO ()
upsertServer_ dbConn SMPServer {host, port, keyHash} = do
execute
dbConn
[sql|
INSERT INTO servers (host, port, key_hash) VALUES (?,?,?)
ON CONFLICT (host, port) DO UPDATE SET
host=excluded.host,
port=excluded.port,
key_hash=excluded.key_hash;
|]
(host, port, keyHash)
-- * createRcvConn helpers
insertRcvQueue_ :: DB.Connection -> ConnId -> RcvQueue -> IO ()
insertRcvQueue_ dbConn connId RcvQueue {..} = do
execute
dbConn
[sql|
INSERT INTO rcv_queues
( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, status)
VALUES
(?,?,?,?,?,?,?,?,?,?);
|]
(host server, port server, rcvId, connId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status)
-- * createSndConn helpers
insertSndQueue_ :: DB.Connection -> ConnId -> SndQueue -> IO ()
insertSndQueue_ dbConn connId SndQueue {..} = do
execute
dbConn
[sql|
INSERT INTO snd_queues
( host, port, snd_id, conn_id, snd_private_key, e2e_dh_secret, status)
VALUES
(?,?,?,?,?,?,?);
|]
(host server, port server, DB.Binary sndId, connId, sndPrivateKey, e2eDhSecret, status)
-- * getConn helpers
getConn_ :: DB.Connection -> ConnId -> IO (Either StoreError SomeConn)
getConn_ dbConn connId =
getConnData_ dbConn connId >>= \case
Nothing -> pure $ Left SEConnNotFound
Just (connData, cMode) -> do
liftIO $ print "before: getRcvQueueByConnId_ dbConn connId"
rQ <- getRcvQueueByConnId_ dbConn connId
liftIO $ print $ "rQ: " <> show rQ
liftIO $ print "before: getSndQueueByConnId_ dbConn connId"
sQ <- getSndQueueByConnId_ dbConn connId
liftIO $ print $ "sQ: " <> show sQ
liftIO $ print "after: getSndQueueByConnId_ dbConn connId"
pure $ case (rQ, sQ, cMode) of
(Just rcvQ, Just sndQ, CMInvitation) -> Right $ SomeConn SCDuplex (DuplexConnection connData rcvQ sndQ)
(Just rcvQ, Nothing, CMInvitation) -> Right $ SomeConn SCRcv (RcvConnection connData rcvQ)
(Nothing, Just sndQ, CMInvitation) -> Right $ SomeConn SCSnd (SndConnection connData sndQ)
(Just rcvQ, Nothing, CMContact) -> Right $ SomeConn SCContact (ContactConnection connData rcvQ)
_ -> Left SEConnNotFound
getConnData_ :: DB.Connection -> ConnId -> IO (Maybe (ConnData, ConnectionMode))
getConnData_ dbConn connId' =
connData
<$> DB.query dbConn "SELECT conn_id, conn_mode FROM connections WHERE conn_id = ?;" (Only connId')
where
connData [(connId, cMode)] = Just (ConnData {connId}, cMode)
connData _ = Nothing
getRcvQueueByConnId_ :: DB.Connection -> ConnId -> IO (Maybe RcvQueue)
getRcvQueueByConnId_ dbConn connId =
rcvQueue
<$> DB.query
dbConn
[sql|
SELECT s.key_hash, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.status
FROM rcv_queues q
INNER JOIN servers s ON q.host = s.host AND q.port = s.port
WHERE q.conn_id = ?;
|]
(Only connId)
where
rcvQueue [(keyHash, host, port, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status)] =
let server = SMPServer host port keyHash
in Just RcvQueue {server, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status}
rcvQueue _ = Nothing
getSndQueueByConnId_ :: DB.Connection -> ConnId -> IO (Maybe SndQueue)
getSndQueueByConnId_ dbConn connId = do
-- sndQueue
-- <$> DB.query
-- dbConn
-- -- [sql|
-- -- SELECT s.key_hash, q.host, q.port, q.snd_id, q.snd_private_key, q.e2e_dh_secret, q.status
-- -- FROM snd_queues q
-- -- INNER JOIN servers s ON q.host = s.host AND q.port = s.port
-- -- WHERE q.conn_id = ?;
-- -- |]
-- [sql|
-- SELECT s.key_hash, q.host, q.port, q.snd_private_key, q.status
-- FROM snd_queues q
-- INNER JOIN servers s ON q.host = s.host AND q.port = s.port
-- WHERE q.conn_id = ?;
-- |]
-- (Only connId)
print "inside: getSndQueueByConnId_"
-- r1 <- (DB.query
-- dbConn
-- [sql|
-- SELECT host, port, key_hash
-- FROM servers
-- WHERE host = ?
-- |]
-- (DB.Only ("localhost" :: HostName))) :: (IO [(HostName, ServiceName, KeyHash)])
-- putStrLn $ show r1
r <- DB.query
dbConn
[sql|
SELECT s.key_hash, q.host, q.port, q.snd_id, q.snd_private_key, q.e2e_dh_secret, q.status
FROM snd_queues q
INNER JOIN servers s ON q.host = s.host AND q.port = s.port
WHERE q.conn_id = ?;
|]
-- [sql|
-- SELECT q.host, q.port, q.status
-- FROM snd_queues q
-- INNER JOIN servers s ON q.host = s.host AND q.port = s.port
-- WHERE q.conn_id = ?;
-- |]
(DB.Only connId)
print $ "r: " <> show r
let q = sndQueue r
print $ "q: " <> show q
pure q
where
sndQueue [(keyHash, host, port, DB.Binary sndId, sndPrivateKey, e2eDhSecret, status)] =
let server = SMPServer host port keyHash
in Just SndQueue {server, sndId, sndPrivateKey, e2eDhSecret, status}
sndQueue _ = Nothing
-- sndQueue [(host, port, status)] = do
-- let server = SMPServer host port "abcd"
-- in Just SndQueue {server, sndId="3456", sndPrivateKey=(C.APrivateSignKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe"), e2eDhSecret="MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=", status}
-- sndQueue _ = Nothing
-- * updateRcvIds helpers
retrieveLastIdsAndHashRcv_ :: DB.Connection -> ConnId -> IO (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
retrieveLastIdsAndHashRcv_ dbConn connId = do
[(lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash)] <-
DB.query
dbConn
[sql|
SELECT last_internal_msg_id, last_internal_rcv_msg_id, last_external_snd_msg_id, last_rcv_msg_hash
FROM connections
WHERE conn_id = ?;
|]
(Only connId)
return (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash)
updateLastIdsRcv_ :: DB.Connection -> ConnId -> InternalId -> InternalRcvId -> IO ()
updateLastIdsRcv_ dbConn connId newInternalId newInternalRcvId =
execute
dbConn
[sql|
UPDATE connections
SET last_internal_msg_id = :last_internal_msg_id,
last_internal_rcv_msg_id = :last_internal_rcv_msg_id
WHERE conn_id = :conn_id;
|]
(newInternalId, newInternalRcvId, connId)
-- * createRcvMsg helpers
insertRcvMsgBase_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()
insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgType, msgBody, internalRcvId} = do
let MsgMeta {recipient = (internalId, internalTs)} = msgMeta
execute
dbConn
[sql|
INSERT INTO messages
( conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_body)
VALUES
(?,?,?,?,NULL,?,?);
|]
(connId, internalId, internalTs, internalRcvId, msgType, msgBody)
insertRcvMsgDetails_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()
insertRcvMsgDetails_ dbConn connId RcvMsgData {msgMeta, internalRcvId, internalHash, externalPrevSndHash} = do
let MsgMeta {integrity, recipient, broker, sndMsgId} = msgMeta
execute
dbConn
[sql|
INSERT INTO rcv_messages
( conn_id, internal_rcv_id, internal_id, external_snd_id,
broker_id, broker_ts,
internal_hash, external_prev_snd_hash, integrity)
VALUES
(?,?,?,?,
?,?,
?,?,?);
|]
(connId, internalRcvId, fst recipient, sndMsgId, fst broker, snd broker, internalHash, externalPrevSndHash, integrity)
updateHashRcv_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()
updateHashRcv_ dbConn connId RcvMsgData {msgMeta, internalHash, internalRcvId} =
execute
dbConn
-- last_internal_rcv_msg_id equality check prevents race condition in case next id was reserved
[sql|
UPDATE connections
SET last_external_snd_msg_id = ?,
last_rcv_msg_hash = ?
WHERE conn_id = ?
AND last_internal_rcv_msg_id = ?;
|]
(sndMsgId (msgMeta :: MsgMeta), internalHash, connId, internalRcvId)
-- * updateSndIds helpers
retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnId -> IO (InternalId, InternalSndId, PrevSndMsgHash)
retrieveLastIdsAndHashSnd_ dbConn connId = do
[(lastInternalId, lastInternalSndId, lastSndHash)] <-
DB.query
dbConn
[sql|
SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash
FROM connections
WHERE conn_id = ?;
|]
(Only connId)
return (lastInternalId, lastInternalSndId, lastSndHash)
updateLastIdsSnd_ :: DB.Connection -> ConnId -> InternalId -> InternalSndId -> IO ()
updateLastIdsSnd_ dbConn connId newInternalId newInternalSndId =
execute
dbConn
[sql|
UPDATE connections
SET last_internal_msg_id = ?,
last_internal_snd_msg_id = ?
WHERE conn_id = ?;
|]
(newInternalId, newInternalSndId, connId)
-- * createSndMsg helpers
insertSndMsgBase_ :: DB.Connection -> ConnId -> SndMsgData -> IO ()
insertSndMsgBase_ dbConn connId SndMsgData {..} = do
execute
dbConn
[sql|
INSERT INTO messages
( conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_body)
VALUES
(?,?,?,NULL,?,?, ?);
|]
(connId, internalId, internalTs, internalSndId, msgType, msgBody)
insertSndMsgDetails_ :: DB.Connection -> ConnId -> SndMsgData -> IO ()
insertSndMsgDetails_ dbConn connId SndMsgData {..} =
execute
dbConn
[sql|
INSERT INTO snd_messages
( conn_id, internal_snd_id, internal_id, internal_hash, previous_msg_hash)
VALUES
(?,?,?,?,?);
|]
(connId, internalSndId, internalId, internalHash, prevMsgHash)
updateHashSnd_ :: DB.Connection -> ConnId -> SndMsgData -> IO ()
updateHashSnd_ dbConn connId SndMsgData {..} =
execute
dbConn
-- last_internal_snd_msg_id equality check prevents race condition in case next id was reserved
[sql|
UPDATE connections
SET last_snd_msg_hash = ?
WHERE conn_id = ?
AND last_internal_snd_msg_id = ?;
|]
(internalHash, connId, internalSndId)
-- create record with a random ID
createWithRandomId :: TVar ChaChaDRG -> (ByteString -> IO ()) -> IO (Either StoreError ByteString)
createWithRandomId gVar create = tryCreate 3
where
tryCreate :: Int -> IO (Either StoreError ByteString)
tryCreate 0 = pure $ Left SEUniqueID
tryCreate n = do
id' <- randomId gVar 12
E.try (create id') >>= \case
Right _ -> pure $ Right id'
Left e -> case constraintViolation e of
Just _ -> tryCreate (n - 1)
Nothing -> pure . Left . SEInternal $ bshow e
randomId :: TVar ChaChaDRG -> Int -> IO ByteString
randomId gVar n = U.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n)
@@ -0,0 +1,73 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Agent.Store.Postgres.Migrations
( Migration (..),
app,
initialize,
get,
run,
)
where
import Control.Monad (forM_, void)
import Data.Function (on)
import Data.List (intercalate, sortBy)
import Data.Time.Clock (getCurrentTime)
import Database.PostgreSQL.Simple (Connection, Only (..))
import qualified Database.PostgreSQL.Simple as DB
import Database.PostgreSQL.Simple.Internal (exec)
import Database.PostgreSQL.Simple.SqlQQ (sql)
import Database.PostgreSQL.Simple.Transaction (withTransaction)
import Database.PostgreSQL.Simple.Types (Query (..))
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20220202_initial (m20220202_initial)
data Migration = Migration {name :: String, up :: Query}
deriving (Show)
schemaMigrations :: [(String, Query)]
schemaMigrations =
[ ("20220101_initial", m20220202_initial)
]
-- | The list of migrations in ascending order by date
app :: [Migration]
app = sortBy (compare `on` name) $ map migration schemaMigrations
where
migration (name, query) = Migration {name, up = query}
get :: Connection -> [Migration] -> IO (Either String [Migration])
get conn migrations =
migrationsToRun migrations . map fromOnly
<$> DB.query_ conn "SELECT name FROM migrations ORDER BY name ASC;"
run :: Connection -> [Migration] -> IO ()
run conn ms = withTransaction conn . forM_ ms $
\Migration {name, up} -> insert name >> exec conn (fromQuery up)
where
insert name = DB.execute conn "INSERT INTO migrations (name, ts) VALUES (?, ?);" . (name,) =<< getCurrentTime
initialize :: Connection -> IO ()
initialize conn =
void $
DB.execute_
conn
[sql|
CREATE TABLE IF NOT EXISTS migrations (
name TEXT NOT NULL,
ts TEXT NOT NULL,
PRIMARY KEY (name)
);
|]
migrationsToRun :: [Migration] -> [String] -> Either String [Migration]
migrationsToRun appMs [] = Right appMs
migrationsToRun [] dbMs = Left $ "database version is newer than the app: " <> intercalate ", " dbMs
migrationsToRun (a : as) (d : ds)
| name a == d = migrationsToRun as ds
| otherwise = Left $ "different migration in the app/database: " <> name a <> " / " <> d
@@ -0,0 +1,158 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20220202_initial where
import Database.PostgreSQL.Simple (Query)
import Database.PostgreSQL.Simple.SqlQQ (sql)
m20220202_initial :: Query
m20220202_initial =
[sql|
-- for easy testing
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
CREATE TABLE servers (
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BYTEA NOT NULL,
PRIMARY KEY (host, port)
);
CREATE TABLE connections (
conn_id BYTEA NOT NULL PRIMARY KEY,
conn_mode TEXT NOT NULL,
last_internal_msg_id INTEGER NOT NULL DEFAULT 0,
last_internal_rcv_msg_id INTEGER NOT NULL DEFAULT 0,
last_internal_snd_msg_id INTEGER NOT NULL DEFAULT 0,
last_external_snd_msg_id INTEGER NOT NULL DEFAULT 0,
last_rcv_msg_hash BYTEA NOT NULL DEFAULT '',
last_snd_msg_hash BYTEA NOT NULL DEFAULT '',
smp_agent_version INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE rcv_queues (
host TEXT NOT NULL,
port TEXT NOT NULL,
rcv_id BYTEA NOT NULL,
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
rcv_private_key BYTEA NOT NULL,
rcv_dh_secret BYTEA NOT NULL,
e2e_priv_key BYTEA NOT NULL,
e2e_dh_secret BYTEA,
snd_id BYTEA NOT NULL,
snd_key BYTEA,
status TEXT NOT NULL,
smp_server_version INTEGER NOT NULL DEFAULT 1,
smp_client_version INTEGER,
PRIMARY KEY (host, port, rcv_id),
FOREIGN KEY (host, port) REFERENCES servers
ON DELETE RESTRICT ON UPDATE CASCADE,
UNIQUE (host, port, snd_id)
);
CREATE TABLE snd_queues (
host TEXT NOT NULL,
port TEXT NOT NULL,
snd_id BYTEA NOT NULL,
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
snd_private_key BYTEA NOT NULL,
e2e_dh_secret BYTEA NOT NULL,
status TEXT NOT NULL,
smp_server_version INTEGER NOT NULL DEFAULT 1,
smp_client_version INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (host, port, snd_id),
FOREIGN KEY (host, port) REFERENCES servers
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE messages (
conn_id BYTEA NOT NULL REFERENCES connections (conn_id)
ON DELETE CASCADE,
internal_id INTEGER NOT NULL,
internal_ts TIMESTAMP NOT NULL,
internal_rcv_id INTEGER,
internal_snd_id INTEGER,
msg_type BYTEA NOT NULL, -- (H)ELLO, (R)EPLY, (D)ELETE. Should SMP confirmation be saved too?
msg_body BYTEA NOT NULL DEFAULT '',
PRIMARY KEY (conn_id, internal_id)
);
CREATE TABLE rcv_messages (
conn_id BYTEA NOT NULL,
internal_rcv_id INTEGER NOT NULL,
internal_id INTEGER NOT NULL,
external_snd_id INTEGER NOT NULL,
broker_id BYTEA NOT NULL,
broker_ts TIMESTAMP NOT NULL,
internal_hash BYTEA NOT NULL,
external_prev_snd_hash BYTEA NOT NULL,
integrity BYTEA NOT NULL, -- in the list of keywords
PRIMARY KEY (conn_id, internal_rcv_id),
FOREIGN KEY (conn_id, internal_id) REFERENCES messages
ON DELETE CASCADE
);
ALTER TABLE messages
ADD CONSTRAINT fk_messages_rcv_messages
FOREIGN KEY (conn_id, internal_rcv_id) REFERENCES rcv_messages
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
CREATE TABLE snd_messages (
conn_id BYTEA NOT NULL,
internal_snd_id INTEGER NOT NULL,
internal_id INTEGER NOT NULL,
internal_hash BYTEA NOT NULL,
previous_msg_hash BYTEA NOT NULL DEFAULT '',
PRIMARY KEY (conn_id, internal_snd_id),
FOREIGN KEY (conn_id, internal_id) REFERENCES messages
ON DELETE CASCADE
);
ALTER TABLE messages
ADD CONSTRAINT fk_messages_snd_messages
FOREIGN KEY (conn_id, internal_snd_id) REFERENCES snd_messages
ON DELETE CASCADE DEFERRABLE INITIALLY deferred;
CREATE TABLE conn_confirmations (
confirmation_id BYTEA NOT NULL PRIMARY KEY,
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
e2e_snd_pub_key BYTEA NOT NULL, -- TODO per-queue key. Split?
sender_key BYTEA NOT NULL, -- TODO per-queue key. Split?
ratchet_state BYTEA NOT NULL,
sender_conn_info BYTEA NOT NULL,
accepted INTEGER NOT NULL,
own_conn_info BYTEA,
created_at TIMESTAMP NOT NULL DEFAULT (now())
);
CREATE TABLE conn_invitations (
invitation_id BYTEA NOT NULL PRIMARY KEY,
contact_conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
cr_invitation BYTEA NOT NULL,
recipient_conn_info BYTEA NOT NULL,
accepted INTEGER NOT NULL DEFAULT 0,
own_conn_info BYTEA,
created_at TIMESTAMP NOT NULL DEFAULT (now())
);
CREATE TABLE ratchets (
conn_id BYTEA NOT NULL PRIMARY KEY REFERENCES connections
ON DELETE CASCADE,
-- x3dh keys are not saved on the sending side (the side accepting the connection)
x3dh_priv_key_1 BYTEA,
x3dh_priv_key_2 BYTEA,
-- ratchet is initially empty on the receiving side (the side offering the connection)
ratchet_state BYTEA,
e2e_version INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE skipped_messages (
skipped_message_id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
conn_id BYTEA NOT NULL REFERENCES ratchets
ON DELETE CASCADE,
header_key BYTEA NOT NULL,
msg_n INTEGER NOT NULL,
msg_key BYTEA NOT NULL
);
|]
@@ -0,0 +1,9 @@
# Postgres setup
Create three databases - `agent_poc_1`, `agent_poc_2`, `agent_poc_3` - and have Postgres server running.
~~`brew install postgresql` - required by postgresql-simple.~~
~~You may run into compilation errors, then you might also need to `brew install libpq --build-from-source`, see [this Stack Overflow answer](https://stackoverflow.com/a/70012033).~~
In the end I managed to build using cabal.
+40 -36
View File
@@ -76,13 +76,13 @@ data SQLiteStore = SQLiteStore
dbNew :: Bool dbNew :: Bool
} }
createSQLiteStore :: FilePath -> Int -> [Migration] -> Bool -> IO SQLiteStore createSQLiteStore :: FilePath -> Int -> [Migration] -> IO SQLiteStore
createSQLiteStore dbFilePath poolSize migrations yesToMigrations = do createSQLiteStore dbFilePath poolSize migrations = do
let dbDir = takeDirectory dbFilePath let dbDir = takeDirectory dbFilePath
createDirectoryIfMissing False dbDir createDirectoryIfMissing False dbDir
st <- connectSQLiteStore dbFilePath poolSize st <- connectSQLiteStore dbFilePath poolSize
checkThreadsafe st checkThreadsafe st
migrateSchema st migrations yesToMigrations migrateSchema st migrations
pure st pure st
checkThreadsafe :: SQLiteStore -> IO () checkThreadsafe :: SQLiteStore -> IO ()
@@ -94,16 +94,15 @@ checkThreadsafe st = withConnection st $ \db -> do
Nothing -> putStrLn "Warning: SQLite THREADSAFE compile option not found" Nothing -> putStrLn "Warning: SQLite THREADSAFE compile option not found"
_ -> return () _ -> return ()
migrateSchema :: SQLiteStore -> [Migration] -> Bool -> IO () migrateSchema :: SQLiteStore -> [Migration] -> IO ()
migrateSchema st migrations yesToMigrations = withConnection st $ \db -> do migrateSchema st migrations = withConnection st $ \db -> do
Migrations.initialize db Migrations.initialize db
Migrations.get db migrations >>= \case Migrations.get db migrations >>= \case
Left e -> confirmOrExit $ "Database error: " <> e Left e -> confirmOrExit $ "Database error: " <> e
Right [] -> pure () Right [] -> pure ()
Right ms -> do Right ms -> do
unless (dbNew st) $ do unless (dbNew st) $ do
unless yesToMigrations $ confirmOrExit "The app has a newer version than the database - it will be backed up and upgraded."
confirmOrExit "The app has a newer version than the database - it will be backed up and upgraded."
let f = dbFilePath st let f = dbFilePath st
copyFile f (f <> ".bak") copyFile f (f <> ".bak")
Migrations.run db ms Migrations.run db ms
@@ -127,25 +126,9 @@ connectSQLiteStore dbFilePath poolSize = do
connectDB :: FilePath -> IO DB.Connection connectDB :: FilePath -> IO DB.Connection
connectDB path = do connectDB path = do
dbConn <- DB.open path dbConn <- DB.open path
DB.execute_ dbConn "PRAGMA foreign_keys = ON;" DB.execute_ dbConn "PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;"
-- DB.execute_ dbConn "PRAGMA trusted_schema = OFF;"
DB.execute_ dbConn "PRAGMA secure_delete = ON;"
DB.execute_ dbConn "PRAGMA auto_vacuum = FULL;"
-- _printPragmas dbConn path
pure dbConn pure dbConn
_printPragmas :: DB.Connection -> FilePath -> IO ()
_printPragmas db path = do
foreign_keys <- DB.query_ db "PRAGMA foreign_keys;" :: IO [[Int]]
print $ path <> " foreign_keys: " <> show foreign_keys
-- when run via sqlite-simple query for trusted_schema seems to return empty list
trusted_schema <- DB.query_ db "PRAGMA trusted_schema;" :: IO [[Int]]
print $ path <> " trusted_schema: " <> show trusted_schema
secure_delete <- DB.query_ db "PRAGMA secure_delete;" :: IO [[Int]]
print $ path <> " secure_delete: " <> show secure_delete
auto_vacuum <- DB.query_ db "PRAGMA auto_vacuum;" :: IO [[Int]]
print $ path <> " auto_vacuum: " <> show auto_vacuum
checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a)
checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err) checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err)
@@ -457,7 +440,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto
insertSndMsgDetails_ db connId sndMsgData insertSndMsgDetails_ db connId sndMsgData
updateHashSnd_ db connId sndMsgData updateHashSnd_ db connId sndMsgData
getPendingMsgData :: SQLiteStore -> ConnId -> InternalId -> m (Maybe RcvQueue, (AgentMessageType, MsgBody, InternalTs)) getPendingMsgData :: SQLiteStore -> ConnId -> InternalId -> m (Maybe RcvQueue, (AMsgType, MsgBody, InternalTs))
getPendingMsgData st connId msgId = getPendingMsgData st connId msgId =
liftIOEither . withTransaction st $ \db -> runExceptT $ do liftIOEither . withTransaction st $ \db -> runExceptT $ do
rq_ <- liftIO $ getRcvQueueByConnId_ db connId rq_ <- liftIO $ getRcvQueueByConnId_ db connId
@@ -582,9 +565,9 @@ instance ToField InternalId where toField (InternalId x) = toField x
instance FromField InternalId where fromField x = InternalId <$> fromField x instance FromField InternalId where fromField x = InternalId <$> fromField x
instance ToField AgentMessageType where toField = toField . smpEncode instance ToField AMsgType where toField = toField . smpEncode
instance FromField AgentMessageType where fromField = blobFieldParser smpP instance FromField AMsgType where fromField = blobFieldParser smpP
instance ToField MsgIntegrity where toField = toField . strEncode instance ToField MsgIntegrity where toField = toField . strEncode
@@ -672,25 +655,46 @@ upsertServer_ dbConn SMPServer {host, port, keyHash} = do
insertRcvQueue_ :: DB.Connection -> ConnId -> RcvQueue -> IO () insertRcvQueue_ :: DB.Connection -> ConnId -> RcvQueue -> IO ()
insertRcvQueue_ dbConn connId RcvQueue {..} = do insertRcvQueue_ dbConn connId RcvQueue {..} = do
DB.execute DB.executeNamed
dbConn dbConn
[sql| [sql|
INSERT INTO rcv_queues INSERT INTO rcv_queues
( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, status) VALUES (?,?,?,?,?,?,?,?,?,?); ( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, status)
VALUES
(:host,:port,:rcv_id,:conn_id,:rcv_private_key,:rcv_dh_secret,:e2e_priv_key,:e2e_dh_secret,:snd_id,:status);
|] |]
(host server, port server, rcvId, connId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status) [ ":host" := host server,
":port" := port server,
":rcv_id" := rcvId,
":conn_id" := connId,
":rcv_private_key" := rcvPrivateKey,
":rcv_dh_secret" := rcvDhSecret,
":e2e_priv_key" := e2ePrivKey,
":e2e_dh_secret" := e2eDhSecret,
":snd_id" := sndId,
":status" := status
]
-- * createSndConn helpers -- * createSndConn helpers
insertSndQueue_ :: DB.Connection -> ConnId -> SndQueue -> IO () insertSndQueue_ :: DB.Connection -> ConnId -> SndQueue -> IO ()
insertSndQueue_ dbConn connId SndQueue {..} = do insertSndQueue_ dbConn connId SndQueue {..} = do
DB.execute DB.executeNamed
dbConn dbConn
[sql| [sql|
INSERT INTO snd_queues INSERT INTO snd_queues
(host, port, snd_id, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, status) VALUES (?,?,?,?,?, ?,?, ?,?); ( host, port, snd_id, conn_id, snd_private_key, e2e_dh_secret, status)
VALUES
(:host,:port,:snd_id,:conn_id,:snd_private_key,:e2e_dh_secret,:status);
|] |]
(host server, port server, sndId, connId, sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret, status) [ ":host" := host server,
":port" := port server,
":snd_id" := sndId,
":conn_id" := connId,
":snd_private_key" := sndPrivateKey,
":e2e_dh_secret" := e2eDhSecret,
":status" := status
]
-- * getConn helpers -- * getConn helpers
@@ -741,16 +745,16 @@ getSndQueueByConnId_ dbConn connId =
<$> DB.query <$> DB.query
dbConn dbConn
[sql| [sql|
SELECT s.key_hash, q.host, q.port, q.snd_id, q.snd_public_key, q.snd_private_key, q.e2e_pub_key, q.e2e_dh_secret, q.status SELECT s.key_hash, q.host, q.port, q.snd_id, q.snd_private_key, q.e2e_dh_secret, q.status
FROM snd_queues q FROM snd_queues q
INNER JOIN servers s ON q.host = s.host AND q.port = s.port INNER JOIN servers s ON q.host = s.host AND q.port = s.port
WHERE q.conn_id = ?; WHERE q.conn_id = ?;
|] |]
(Only connId) (Only connId)
where where
sndQueue [(keyHash, host, port, sndId, sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret, status)] = sndQueue [(keyHash, host, port, sndId, sndPrivateKey, e2eDhSecret, status)] =
let server = SMPServer host port keyHash let server = SMPServer host port keyHash
in Just SndQueue {server, sndId, sndPublicKey, sndPrivateKey, e2ePubKey, e2eDhSecret, status} in Just SndQueue {server, sndId, sndPrivateKey, e2eDhSecret, status}
sndQueue _ = Nothing sndQueue _ = Nothing
-- * updateRcvIds helpers -- * updateRcvIds helpers
@@ -25,15 +25,13 @@ import qualified Database.SQLite.Simple as DB
import Database.SQLite.Simple.QQ (sql) import Database.SQLite.Simple.QQ (sql)
import qualified Database.SQLite3 as SQLite3 import qualified Database.SQLite3 as SQLite3
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
data Migration = Migration {name :: String, up :: Text} data Migration = Migration {name :: String, up :: Text}
deriving (Show) deriving (Show)
schemaMigrations :: [(String, Query)] schemaMigrations :: [(String, Query)]
schemaMigrations = schemaMigrations =
[ ("20220101_initial", m20220101_initial), [ ("20220101_initial", m20220101_initial)
("20220301_snd_queue_keys", m20220301_snd_queue_keys)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date
@@ -1,13 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20220301_snd_queue_keys :: Query
m20220301_snd_queue_keys =
[sql|
ALTER TABLE snd_queues ADD COLUMN snd_public_key BLOB;
ALTER TABLE snd_queues ADD COLUMN e2e_pub_key BLOB;
|]
+28 -31
View File
@@ -23,7 +23,7 @@
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
module Simplex.Messaging.Client module Simplex.Messaging.Client
( -- * Connect (disconnect) client to (from) SMP server ( -- * Connect (disconnect) client to (from) SMP server
SMPClient (sessionId), SMPClient,
getSMPClient, getSMPClient,
closeSMPClient, closeSMPClient,
@@ -56,16 +56,15 @@ import Control.Monad.Trans.Class
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Network.Socket (ServiceName) import Network.Socket (ServiceName)
import Numeric.Natural import Numeric.Natural
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol import Simplex.Messaging.Protocol
import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (ATransport (..), THandle (..), TLS, TProxy, Transport (..), TransportError, clientHandshake)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client (runTransportClient) import Simplex.Messaging.Transport.Client (runTransportClient)
import Simplex.Messaging.Transport.KeepAlive
import Simplex.Messaging.Transport.WebSockets (WS) import Simplex.Messaging.Transport.WebSockets (WS)
import Simplex.Messaging.Util (bshow, liftError, raceAny_) import Simplex.Messaging.Util (bshow, liftError, raceAny_)
import System.Timeout (timeout) import System.Timeout (timeout)
@@ -79,18 +78,18 @@ import System.Timeout (timeout)
data SMPClient = SMPClient data SMPClient = SMPClient
{ action :: Async (), { action :: Async (),
connected :: TVar Bool, connected :: TVar Bool,
sessionId :: SessionId, sessionId :: ByteString,
smpServer :: SMPServer, smpServer :: SMPServer,
tcpTimeout :: Int, tcpTimeout :: Int,
clientCorrId :: TVar Natural, clientCorrId :: TVar Natural,
sentCommands :: TMap CorrId Request, sentCommands :: TVar (Map CorrId Request),
sndQ :: TBQueue SentRawTransmission, sndQ :: TBQueue SentRawTransmission,
rcvQ :: TBQueue (SignedTransmission BrokerMsg), rcvQ :: TBQueue (SignedTransmission BrokerMsg),
msgQ :: TBQueue SMPServerTransmission msgQ :: TBQueue SMPServerTransmission
} }
-- | Type synonym for transmission from some SPM server queue. -- | Type synonym for transmission from some SPM server queue.
type SMPServerTransmission = (SMPServer, SessionId, RecipientId, BrokerMsg) type SMPServerTransmission = (SMPServer, RecipientId, BrokerMsg)
-- | SMP client configuration. -- | SMP client configuration.
data SMPClientConfig = SMPClientConfig data SMPClientConfig = SMPClientConfig
@@ -100,8 +99,6 @@ data SMPClientConfig = SMPClientConfig
defaultTransport :: (ServiceName, ATransport), defaultTransport :: (ServiceName, ATransport),
-- | timeout of TCP commands (microseconds) -- | timeout of TCP commands (microseconds)
tcpTimeout :: Int, tcpTimeout :: Int,
-- | TCP keep-alive options, Nothing to skip enabling keep-alive
tcpKeepAlive :: Maybe KeepAliveOpts,
-- | period for SMP ping commands (microseconds) -- | period for SMP ping commands (microseconds)
smpPing :: Int smpPing :: Int
} }
@@ -110,11 +107,10 @@ data SMPClientConfig = SMPClientConfig
smpDefaultConfig :: SMPClientConfig smpDefaultConfig :: SMPClientConfig
smpDefaultConfig = smpDefaultConfig =
SMPClientConfig SMPClientConfig
{ qSize = 64, { qSize = 16,
defaultTransport = ("5223", transport @TLS), defaultTransport = ("5223", transport @TLS),
tcpTimeout = 5_000_000, tcpTimeout = 4_000_000,
tcpKeepAlive = Just defaultKeepAliveOpts, smpPing = 30_000_000
smpPing = 600_000_000 -- 10min
} }
data Request = Request data Request = Request
@@ -130,14 +126,14 @@ type Response = Either SMPClientError BrokerMsg
-- A single queue can be used for multiple 'SMPClient' instances, -- A single queue can be used for multiple 'SMPClient' instances,
-- as 'SMPServerTransmission' includes server information. -- as 'SMPServerTransmission' includes server information.
getSMPClient :: SMPServer -> SMPClientConfig -> TBQueue SMPServerTransmission -> IO () -> IO (Either SMPClientError SMPClient) getSMPClient :: SMPServer -> SMPClientConfig -> TBQueue SMPServerTransmission -> IO () -> IO (Either SMPClientError SMPClient)
getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, tcpKeepAlive, smpPing} msgQ disconnected = getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ disconnected =
atomically mkSMPClient >>= runClient useTransport atomically mkSMPClient >>= runClient useTransport
where where
mkSMPClient :: STM SMPClient mkSMPClient :: STM SMPClient
mkSMPClient = do mkSMPClient = do
connected <- newTVar False connected <- newTVar False
clientCorrId <- newTVar 0 clientCorrId <- newTVar 0
sentCommands <- TM.empty sentCommands <- newTVar M.empty
sndQ <- newTBQueue qSize sndQ <- newTBQueue qSize
rcvQ <- newTBQueue qSize rcvQ <- newTBQueue qSize
return return
@@ -159,7 +155,7 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, tcpKeepAlive, smp
thVar <- newEmptyTMVarIO thVar <- newEmptyTMVarIO
action <- action <-
async $ async $
runTransportClient (host smpServer) port' (keyHash smpServer) tcpKeepAlive (client t c thVar) runTransportClient (host smpServer) port' (keyHash smpServer) (client t c thVar)
`finally` atomically (putTMVar thVar $ Left SMPNetworkError) `finally` atomically (putTMVar thVar $ Left SMPNetworkError)
th_ <- tcpTimeout `timeout` atomically (takeTMVar thVar) th_ <- tcpTimeout `timeout` atomically (takeTMVar thVar)
pure $ case th_ of pure $ case th_ of
@@ -197,15 +193,16 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, tcpKeepAlive, smp
runExceptT $ sendSMPCommand c Nothing "" PING runExceptT $ sendSMPCommand c Nothing "" PING
process :: SMPClient -> IO () process :: SMPClient -> IO ()
process SMPClient {sessionId, rcvQ, sentCommands} = forever $ do process SMPClient {rcvQ, sentCommands} = forever $ do
(_, _, (corrId, qId, respOrErr)) <- atomically $ readTBQueue rcvQ (_, _, (corrId, qId, respOrErr)) <- atomically $ readTBQueue rcvQ
if B.null $ bs corrId if B.null $ bs corrId
then sendMsg qId respOrErr then sendMsg qId respOrErr
else do else do
atomically (TM.lookup corrId sentCommands) >>= \case cs <- readTVarIO sentCommands
case M.lookup corrId cs of
Nothing -> sendMsg qId respOrErr Nothing -> sendMsg qId respOrErr
Just Request {queueId, responseVar} -> atomically $ do Just Request {queueId, responseVar} -> atomically $ do
TM.delete corrId sentCommands modifyTVar sentCommands $ M.delete corrId
putTMVar responseVar $ putTMVar responseVar $
if queueId == qId if queueId == qId
then case respOrErr of then case respOrErr of
@@ -213,12 +210,12 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, tcpKeepAlive, smp
Right (ERR e) -> Left $ SMPServerError e Right (ERR e) -> Left $ SMPServerError e
Right r -> Right r Right r -> Right r
else Left SMPUnexpectedResponse else Left SMPUnexpectedResponse
where
sendMsg :: QueueId -> Either ErrorType BrokerMsg -> IO () sendMsg :: QueueId -> Either ErrorType BrokerMsg -> IO ()
sendMsg qId = \case sendMsg qId = \case
Right cmd -> atomically $ writeTBQueue msgQ (smpServer, sessionId, qId, cmd) Right cmd -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd)
-- TODO send everything else to errQ and log in agent -- TODO send everything else to errQ and log in agent
_ -> return () _ -> return ()
-- | Disconnects SMP client from the server and terminates client threads. -- | Disconnects SMP client from the server and terminates client threads.
closeSMPClient :: SMPClient -> IO () closeSMPClient :: SMPClient -> IO ()
@@ -268,11 +265,11 @@ createSMPQueue c rpKey rKey dhKey =
-- --
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue
subscribeSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO () subscribeSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO ()
subscribeSMPQueue c@SMPClient {smpServer, sessionId, msgQ} rpKey rId = subscribeSMPQueue c@SMPClient {smpServer, msgQ} rpKey rId =
sendSMPCommand c (Just rpKey) rId SUB >>= \case sendSMPCommand c (Just rpKey) rId SUB >>= \case
OK -> return () OK -> return ()
cmd@MSG {} -> cmd@MSG {} ->
lift . atomically $ writeTBQueue msgQ (smpServer, sessionId, rId, cmd) lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
_ -> throwE SMPUnexpectedResponse _ -> throwE SMPUnexpectedResponse
-- | Subscribe to the SMP queue notifications. -- | Subscribe to the SMP queue notifications.
@@ -309,11 +306,11 @@ sendSMPMessage c spKey sId msg =
-- --
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
ackSMPMessage :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT SMPClientError IO () ackSMPMessage :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
ackSMPMessage c@SMPClient {smpServer, sessionId, msgQ} rpKey rId = ackSMPMessage c@SMPClient {smpServer, msgQ} rpKey rId =
sendSMPCommand c (Just rpKey) rId ACK >>= \case sendSMPCommand c (Just rpKey) rId ACK >>= \case
OK -> return () OK -> return ()
cmd@MSG {} -> cmd@MSG {} ->
lift . atomically $ writeTBQueue msgQ (smpServer, sessionId, rId, cmd) lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
_ -> throwE SMPUnexpectedResponse _ -> throwE SMPUnexpectedResponse
-- | Irreversibly suspend SMP queue. -- | Irreversibly suspend SMP queue.
@@ -367,6 +364,6 @@ sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, sessionId, tcpTimeou
send :: CorrId -> SentRawTransmission -> STM (TMVar Response) send :: CorrId -> SentRawTransmission -> STM (TMVar Response)
send corrId t = do send corrId t = do
r <- newEmptyTMVar r <- newEmptyTMVar
TM.insert corrId (Request qId r) sentCommands modifyTVar sentCommands . M.insert corrId $ Request qId r
writeTBQueue sndQ t writeTBQueue sndQ t
return r return r
+77 -20
View File
@@ -149,14 +149,20 @@ import Data.String
import Data.Type.Equality import Data.Type.Equality
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import Data.X509 import Data.X509
import Database.SQLite.Simple.FromField (FromField (..)) import qualified Database.PostgreSQL.Simple as PDB
import Database.SQLite.Simple.ToField (ToField (..)) import qualified Database.PostgreSQL.Simple.FromField as PF
import qualified Database.PostgreSQL.Simple.ToField as PT
import qualified Database.PostgreSQL.Simple.TypeInfo as PTI
import qualified Database.PostgreSQL.Simple.TypeInfo.Static as PTIS
import qualified Database.SQLite.Simple.FromField as SF
import qualified Database.SQLite.Simple.ToField as ST
import GHC.TypeLits (ErrorMessage (..), TypeError) import GHC.TypeLits (ErrorMessage (..), TypeError)
import Network.Transport.Internal (decodeWord16, encodeWord16) import Network.Transport.Internal (decodeWord16, encodeWord16)
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (blobFieldDecoder, parseAll, parseString) import Simplex.Messaging.Parsers (blobFieldDecoder, parseAll, parseString)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))
import qualified Database.PostgreSQL.Simple as PDB
-- | Cryptographic algorithms. -- | Cryptographic algorithms.
data Algorithm = Ed25519 | Ed448 | X25519 | X448 data Algorithm = Ed25519 | Ed448 | X25519 | X448
@@ -540,33 +546,62 @@ generateKeyPair' = case sAlgorithm @a of
let k = X448.toPublic pk let k = X448.toPublic pk
in pure (PublicKeyX448 k, PrivateKeyX448 pk k) in pure (PublicKeyX448 k, PrivateKeyX448 pk k)
instance ToField APrivateSignKey where toField = toField . encodePrivKey instance ST.ToField APrivateSignKey where toField = ST.toField . encodePrivKey
instance ToField APublicVerifyKey where toField = toField . encodePubKey instance ST.ToField APublicVerifyKey where toField = ST.toField . encodePubKey
instance ToField APrivateDhKey where toField = toField . encodePrivKey instance ST.ToField APrivateDhKey where toField = ST.toField . encodePrivKey
instance ToField APublicDhKey where toField = toField . encodePubKey instance ST.ToField APublicDhKey where toField = ST.toField . encodePubKey
instance AlgorithmI a => ToField (PrivateKey a) where toField = toField . encodePrivKey instance AlgorithmI a => ST.ToField (PrivateKey a) where toField = ST.toField . encodePrivKey
instance AlgorithmI a => ToField (PublicKey a) where toField = toField . encodePubKey instance AlgorithmI a => ST.ToField (PublicKey a) where toField = ST.toField . encodePubKey
instance ToField (DhSecret a) where toField = toField . dhBytes' instance ST.ToField (DhSecret a) where toField = ST.toField . dhBytes'
instance FromField APrivateSignKey where fromField = blobFieldDecoder decodePrivKey instance SF.FromField APrivateSignKey where fromField = blobFieldDecoder decodePrivKey
instance FromField APublicVerifyKey where fromField = blobFieldDecoder decodePubKey instance SF.FromField APublicVerifyKey where fromField = blobFieldDecoder decodePubKey
instance FromField APrivateDhKey where fromField = blobFieldDecoder decodePrivKey instance SF.FromField APrivateDhKey where fromField = blobFieldDecoder decodePrivKey
instance FromField APublicDhKey where fromField = blobFieldDecoder decodePubKey instance SF.FromField APublicDhKey where fromField = blobFieldDecoder decodePubKey
instance (Typeable a, AlgorithmI a) => FromField (PrivateKey a) where fromField = blobFieldDecoder decodePrivKey instance (Typeable a, AlgorithmI a) => SF.FromField (PrivateKey a) where fromField = blobFieldDecoder decodePrivKey
instance (Typeable a, AlgorithmI a) => FromField (PublicKey a) where fromField = blobFieldDecoder decodePubKey instance (Typeable a, AlgorithmI a) => SF.FromField (PublicKey a) where fromField = blobFieldDecoder decodePubKey
instance (Typeable a, AlgorithmI a) => FromField (DhSecret a) where fromField = blobFieldDecoder strDecode instance (Typeable a, AlgorithmI a) => SF.FromField (DhSecret a) where fromField = blobFieldDecoder strDecode
instance PT.ToField APrivateSignKey where toField = PT.toField . encodePrivKey
instance PT.ToField APublicVerifyKey where toField = PT.toField . encodePubKey
instance PT.ToField APrivateDhKey where toField = PT.toField . encodePrivKey
instance PT.ToField APublicDhKey where toField = PT.toField . encodePubKey
instance AlgorithmI a => PT.ToField (PrivateKey a) where toField = PT.toField . encodePrivKey
instance AlgorithmI a => PT.ToField (PublicKey a) where toField = PT.toField . encodePubKey
instance PT.ToField (DhSecret a) where toField = PT.toField . PDB.Binary . dhBytes'
instance PF.FromField APrivateSignKey where fromField = fromByteStringField decodePrivKey
instance PF.FromField APublicVerifyKey where fromField = fromByteStringField decodePubKey
instance PF.FromField APrivateDhKey where fromField = fromByteStringField decodePrivKey
instance PF.FromField APublicDhKey where fromField = fromByteStringField decodePubKey
instance (Typeable a, AlgorithmI a) => PF.FromField (PrivateKey a) where fromField = fromByteStringField decodePrivKey
instance (Typeable a, AlgorithmI a) => PF.FromField (PublicKey a) where fromField = fromByteStringField decodePubKey
-- instance (Typeable a, AlgorithmI a) => PF.FromField (DhSecret a) where fromField = fromByteStringField strDecode
instance (Typeable a, AlgorithmI a) => PF.FromField (DhSecret a) where fromField x = fromByteStringField strDecode x
instance IsString (Maybe ASignature) where instance IsString (Maybe ASignature) where
fromString = parseString $ decode >=> decodeSignature fromString = parseString $ decode >=> decodeSignature
@@ -690,9 +725,13 @@ validSignatureSize n =
newtype Key = Key {unKey :: ByteString} newtype Key = Key {unKey :: ByteString}
deriving (Eq, Ord, Show) deriving (Eq, Ord, Show)
instance ToField Key where toField = toField . unKey instance ST.ToField Key where toField = ST.toField . unKey
instance FromField Key where fromField f = Key <$> fromField f instance PT.ToField Key where toField = PT.toField . unKey
instance SF.FromField Key where fromField f = Key <$> SF.fromField f
instance PF.FromField Key where fromField f = PF.fromField f
instance ToJSON Key where instance ToJSON Key where
toJSON = strToJSON . unKey toJSON = strToJSON . unKey
@@ -730,9 +769,27 @@ instance StrEncoding KeyHash where
instance IsString KeyHash where instance IsString KeyHash where
fromString = parseString $ parseAll strP fromString = parseString $ parseAll strP
instance ToField KeyHash where toField = toField . strEncode instance ST.ToField KeyHash where toField = ST.toField . strEncode
instance FromField KeyHash where fromField = blobFieldDecoder $ parseAll strP instance SF.FromField KeyHash where fromField = blobFieldDecoder $ parseAll strP
instance PT.ToField KeyHash where toField = PT.toField . strEncode
-- TODO
-- instance PF.FromField KeyHash where fromField = blobFieldDecoderPostgres $ parseAll strP
instance PF.FromField KeyHash where fromField = fromByteStringField $ parseAll strP
fromByteStringField :: Typeable a => (ByteString -> Either String a) -> PF.Field -> Maybe ByteString -> PF.Conversion a
fromByteStringField dec f mdata =
if PF.typeOid f /= PTI.typoid PTIS.bytea
then PF.returnError PF.Incompatible f ""
else case mdata of
Nothing -> PF.returnError PF.UnexpectedNull f ""
Just dat ->
case dec dat of
Right x -> return x
_ -> PF.returnError PF.ConversionFailed f (B.unpack dat)
-- | SHA256 digest. -- | SHA256 digest.
sha256Hash :: ByteString -> ByteString sha256Hash :: ByteString -> ByteString
+29 -6
View File
@@ -30,8 +30,12 @@ import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import Data.Word (Word32) import Data.Word (Word32)
import Database.SQLite.Simple.FromField (FromField (..)) import qualified Database.PostgreSQL.Simple.FromField as PF
import Database.SQLite.Simple.ToField (ToField (..)) import qualified Database.PostgreSQL.Simple.ToField as PT
import qualified Database.PostgreSQL.Simple.TypeInfo as PTI
import qualified Database.PostgreSQL.Simple.TypeInfo.Static as PTIS
import qualified Database.SQLite.Simple.FromField as SF
import qualified Database.SQLite.Simple.ToField as ST
import GHC.Generics import GHC.Generics
import Simplex.Messaging.Agent.QueryString import Simplex.Messaging.Agent.QueryString
import Simplex.Messaging.Crypto import Simplex.Messaging.Crypto
@@ -197,13 +201,32 @@ instance ToJSON RatchetKey where
instance FromJSON RatchetKey where instance FromJSON RatchetKey where
parseJSON = fmap RatchetKey . strParseJSON "Key" parseJSON = fmap RatchetKey . strParseJSON "Key"
instance AlgorithmI a => ToField (Ratchet a) where toField = toField . LB.toStrict . J.encode instance AlgorithmI a => ST.ToField (Ratchet a) where toField = ST.toField . LB.toStrict . J.encode
instance (AlgorithmI a, Typeable a) => FromField (Ratchet a) where fromField = blobFieldDecoder J.eitherDecodeStrict' instance AlgorithmI a => PT.ToField (Ratchet a) where toField = PT.toField . LB.toStrict . J.encode
instance ToField MessageKey where toField = toField . smpEncode instance (AlgorithmI a, Typeable a) => PF.FromField (Ratchet a) where fromField = fromByteStringField J.eitherDecodeStrict'
instance FromField MessageKey where fromField = blobFieldDecoder smpDecode instance (AlgorithmI a, Typeable a) => SF.FromField (Ratchet a) where fromField = blobFieldDecoder J.eitherDecodeStrict'
instance ST.ToField MessageKey where toField = ST.toField . smpEncode
instance PT.ToField MessageKey where toField = PT.toField . smpEncode
instance SF.FromField MessageKey where fromField = blobFieldDecoder smpDecode
instance PF.FromField MessageKey where fromField = fromByteStringField smpDecode
fromByteStringField :: Typeable a => (ByteString -> Either String a) -> PF.Field -> Maybe ByteString -> PF.Conversion a
fromByteStringField dec f mdata =
if PF.typeOid f /= PTI.typoid PTIS.bytea
then PF.returnError PF.Incompatible f ""
else case mdata of
Nothing -> PF.returnError PF.UnexpectedNull f ""
Just dat ->
case dec dat of
Right x -> return x
_ -> PF.returnError PF.ConversionFailed f (B.unpack dat)
-- | Sending ratchet initialization, equivalent to RatchetInitAliceHE in double ratchet spec -- | Sending ratchet initialization, equivalent to RatchetInitAliceHE in double ratchet spec
-- --
+20 -16
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
@@ -16,10 +15,13 @@ import Data.Char (isAlphaNum, toLower)
import Data.Time.Clock (UTCTime) import Data.Time.Clock (UTCTime)
import Data.Time.ISO8601 (parseISO8601) import Data.Time.ISO8601 (parseISO8601)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import qualified Database.PostgreSQL.Simple.FromField as PF
import qualified Database.PostgreSQL.Simple.Internal as PI
import qualified Database.PostgreSQL.Simple.Ok as PO
import Database.SQLite.Simple (ResultError (..), SQLData (..)) import Database.SQLite.Simple (ResultError (..), SQLData (..))
import Database.SQLite.Simple.FromField (FieldParser, returnError) import qualified Database.SQLite.Simple.FromField as SF
import Database.SQLite.Simple.Internal (Field (..)) import qualified Database.SQLite.Simple.Internal as SI
import Database.SQLite.Simple.Ok (Ok (Ok)) import qualified Database.SQLite.Simple.Ok as SO
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))
import Text.Read (readMaybe) import Text.Read (readMaybe)
@@ -70,16 +72,24 @@ wordEnd c = c == ' ' || c == '\n'
parseString :: (ByteString -> Either String a) -> (String -> a) parseString :: (ByteString -> Either String a) -> (String -> a)
parseString p = either error id . p . B.pack parseString p = either error id . p . B.pack
blobFieldParser :: Typeable k => Parser k -> FieldParser k blobFieldParser :: Typeable k => Parser k -> SF.FieldParser k
blobFieldParser = blobFieldDecoder . parseAll blobFieldParser = blobFieldDecoder . parseAll
blobFieldDecoder :: Typeable k => (ByteString -> Either String k) -> FieldParser k blobFieldDecoder :: Typeable k => (ByteString -> Either String k) -> SF.FieldParser k
blobFieldDecoder dec = \case blobFieldDecoder dec = \case
f@(Field (SQLBlob b) _) -> f@(SI.Field (SQLBlob b) _) ->
case dec b of case dec b of
Right k -> Ok k Right k -> SO.Ok k
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e) Left e -> SF.returnError SF.ConversionFailed f ("couldn't parse field: " ++ e)
f -> returnError ConversionFailed f "expecting SQLBlob column type" f -> SF.returnError SF.ConversionFailed f "expecting SQLBlob column type"
-- blobFieldDecoderPostgres :: Typeable k => (ByteString -> Either String k) -> PF.FieldParser k
-- blobFieldDecoderPostgres dec = \case
-- f@(PI.Field b _ _) ->
-- case dec b of
-- Right k -> PO.Ok k
-- Left e -> PF.returnError PF.ConversionFailed f ("couldn't parse field: " ++ e)
-- f -> PF.returnError PF.ConversionFailed f "expecting SQLBlob column type"
fstToLower :: String -> String fstToLower :: String -> String
fstToLower "" = "" fstToLower "" = ""
@@ -98,18 +108,13 @@ enumJSON tagModifier =
} }
sumTypeJSON :: (String -> String) -> J.Options sumTypeJSON :: (String -> String) -> J.Options
#if defined(darwin_HOST_OS) && defined(swiftJSON)
sumTypeJSON = singleFieldJSON sumTypeJSON = singleFieldJSON
#else
sumTypeJSON = taggedObjectJSON
#endif
taggedObjectJSON :: (String -> String) -> J.Options taggedObjectJSON :: (String -> String) -> J.Options
taggedObjectJSON tagModifier = taggedObjectJSON tagModifier =
J.defaultOptions J.defaultOptions
{ J.sumEncoding = J.TaggedObject "type" "data", { J.sumEncoding = J.TaggedObject "type" "data",
J.constructorTagModifier = tagModifier, J.constructorTagModifier = tagModifier,
J.allNullaryToStringTag = False,
J.nullaryToObject = True, J.nullaryToObject = True,
J.omitNothingFields = True J.omitNothingFields = True
} }
@@ -119,7 +124,6 @@ singleFieldJSON tagModifier =
J.defaultOptions J.defaultOptions
{ J.sumEncoding = J.ObjectWithSingleField, { J.sumEncoding = J.ObjectWithSingleField,
J.constructorTagModifier = tagModifier, J.constructorTagModifier = tagModifier,
J.allNullaryToStringTag = False,
J.nullaryToObject = True, J.nullaryToObject = True,
J.omitNothingFields = True J.omitNothingFields = True
} }
+3 -7
View File
@@ -108,7 +108,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers import Simplex.Messaging.Parsers
import Simplex.Messaging.Transport (SessionId, THandle (..), Transport, TransportError (..), tGetBlock, tPutBlock) import Simplex.Messaging.Transport (THandle (..), Transport, TransportError (..), tGetBlock, tPutBlock)
import Simplex.Messaging.Util (bshow, (<$?>)) import Simplex.Messaging.Util (bshow, (<$?>))
import Simplex.Messaging.Version import Simplex.Messaging.Version
import Test.QuickCheck (Arbitrary (..)) import Test.QuickCheck (Arbitrary (..))
@@ -172,14 +172,14 @@ type Signed = ByteString
data RawTransmission = RawTransmission data RawTransmission = RawTransmission
{ signature :: ByteString, { signature :: ByteString,
signed :: ByteString, signed :: ByteString,
sessId :: SessionId, sessId :: ByteString,
corrId :: ByteString, corrId :: ByteString,
queueId :: ByteString, queueId :: ByteString,
command :: ByteString command :: ByteString
} }
-- | unparsed sent SMP transmission with signature, without session ID. -- | unparsed sent SMP transmission with signature, without session ID.
type SignedRawTransmission = (Maybe C.ASignature, SessionId, ByteString, ByteString) type SignedRawTransmission = (Maybe C.ASignature, ByteString, ByteString, ByteString)
-- | unparsed sent SMP transmission with signature. -- | unparsed sent SMP transmission with signature.
type SentRawTransmission = (Maybe C.ASignature, ByteString) type SentRawTransmission = (Maybe C.ASignature, ByteString)
@@ -399,10 +399,6 @@ instance StrEncoding SMPServer where
SrvLoc host port <- strP SrvLoc host port <- strP
pure SMPServer {host, port, keyHash} pure SMPServer {host, port, keyHash}
instance ToJSON SMPServer where
toJSON = strToJSON
toEncoding = strToJEncoding
data SrvLoc = SrvLoc HostName ServiceName data SrvLoc = SrvLoc HostName ServiceName
deriving (Eq, Ord, Show) deriving (Eq, Ord, Show)
+39 -61
View File
@@ -25,6 +25,7 @@
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
module Simplex.Messaging.Server (runSMPServer, runSMPServerBlocking) where module Simplex.Messaging.Server (runSMPServer, runSMPServerBlocking) where
import Control.Concurrent.STM (stateTVar)
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.IO.Unlift import Control.Monad.IO.Unlift
@@ -33,10 +34,9 @@ import Crypto.Random
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.Functor (($>)) import Data.Functor (($>))
import Data.Int (Int64)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (isNothing) import Data.Maybe (isNothing)
import Data.Time.Clock.System (SystemTime (..), getSystemTime) import Data.Time.Clock.System (getSystemTime)
import Data.Type.Equality import Data.Type.Equality
import Network.Socket (ServiceName) import Network.Socket (ServiceName)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
@@ -47,8 +47,6 @@ import Simplex.Messaging.Server.MsgStore.STM (MsgQueue)
import Simplex.Messaging.Server.QueueStore import Simplex.Messaging.Server.QueueStore
import Simplex.Messaging.Server.QueueStore.STM (QueueStore) import Simplex.Messaging.Server.QueueStore.STM (QueueStore)
import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Server import Simplex.Messaging.Transport.Server
import Simplex.Messaging.Util import Simplex.Messaging.Util
@@ -70,32 +68,34 @@ runSMPServer cfg = do
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True) -- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
-- and when it is disconnected from the TCP socket once the server thread is killed (False). -- and when it is disconnected from the TCP socket once the server thread is killed (False).
runSMPServerBlocking :: (MonadRandom m, MonadUnliftIO m) => TMVar Bool -> ServerConfig -> m () runSMPServerBlocking :: (MonadRandom m, MonadUnliftIO m) => TMVar Bool -> ServerConfig -> m ()
runSMPServerBlocking started cfg = newEnv cfg >>= runReaderT (smpServer started) runSMPServerBlocking started cfg@ServerConfig {transports} = do
env <- newEnv cfg
smpServer :: forall m. (MonadUnliftIO m, MonadReader Env m) => TMVar Bool -> m () runReaderT smpServer env
smpServer started = do
s <- asks server
cfg@ServerConfig {transports} <- asks config
raceAny_
( serverThread s subscribedQ subscribers subscriptions cancelSub :
serverThread s ntfSubscribedQ notifiers ntfSubscriptions (\_ -> pure ()) :
map runServer transports <> expireMessagesThread_ cfg
)
`finally` withLog closeStoreLog
where where
runServer :: (ServiceName, ATransport) -> m () smpServer :: (MonadUnliftIO m', MonadReader Env m') => m' ()
smpServer = do
s <- asks server
raceAny_
( serverThread s subscribedQ subscribers subscriptions cancelSub :
serverThread s ntfSubscribedQ notifiers ntfSubscriptions (\_ -> pure ()) :
map runServer transports
)
`finally` withLog closeStoreLog
runServer :: (MonadUnliftIO m', MonadReader Env m') => (ServiceName, ATransport) -> m' ()
runServer (tcpPort, ATransport t) = do runServer (tcpPort, ATransport t) = do
serverParams <- asks tlsServerParams serverParams <- asks tlsServerParams
runTransportServer started tcpPort serverParams (runClient t) runTransportServer started tcpPort serverParams (runClient t)
serverThread :: serverThread ::
forall s. forall m' s.
MonadUnliftIO m' =>
Server -> Server ->
(Server -> TBQueue (QueueId, Client)) -> (Server -> TBQueue (QueueId, Client)) ->
(Server -> TMap QueueId Client) -> (Server -> TVar (M.Map QueueId Client)) ->
(Client -> TMap QueueId s) -> (Client -> TVar (M.Map QueueId s)) ->
(s -> m ()) -> (s -> m' ()) ->
m () m' ()
serverThread s subQ subs clientSubs unsub = forever $ do serverThread s subQ subs clientSubs unsub = forever $ do
atomically updateSubscribers atomically updateSubscribers
>>= fmap join . mapM endPreviousSubscriptions >>= fmap join . mapM endPreviousSubscriptions
@@ -110,33 +110,15 @@ smpServer started = do
else do else do
yes <- readTVar $ connected c' yes <- readTVar $ connected c'
pure $ if yes then Just (qId, c') else Nothing pure $ if yes then Just (qId, c') else Nothing
TM.lookupInsert qId clnt (subs s) stateTVar (subs s) (\cs -> (M.lookup qId cs, M.insert qId clnt cs))
>>= fmap join . mapM clientToBeNotified >>= fmap join . mapM clientToBeNotified
endPreviousSubscriptions :: (QueueId, Client) -> m (Maybe s) endPreviousSubscriptions :: (QueueId, Client) -> m' (Maybe s)
endPreviousSubscriptions (qId, c) = do endPreviousSubscriptions (qId, c) = do
void . forkIO . atomically $ void . forkIO . atomically $
writeTBQueue (sndQ c) (CorrId "", qId, END) writeTBQueue (sndQ c) (CorrId "", qId, END)
atomically $ TM.lookupDelete qId (clientSubs c) atomically . stateTVar (clientSubs c) $ \ss -> (M.lookup qId ss, M.delete qId ss)
expireMessagesThread_ :: ServerConfig -> [m ()] runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m ()
expireMessagesThread_ ServerConfig {messageTTL, expireMessagesInterval} =
case (messageTTL, expireMessagesInterval) of
(Just ttl, Just int) -> [expireMessages ttl int]
_ -> []
expireMessages :: Int64 -> Int -> m ()
expireMessages ttl interval = do
ms <- asks msgStore
quota <- asks $ msgQueueQuota . config
forever $ do
threadDelay interval
old <- subtract ttl . systemSeconds <$> liftIO getSystemTime
rIds <- M.keysSet <$> readTVarIO ms
forM_ rIds $ \rId ->
atomically (getMsgQueue ms rId quota)
>>= atomically . (`deleteExpiredMsgs` old)
runClient :: Transport c => TProxy c -> c -> m ()
runClient _ h = do runClient _ h = do
kh <- asks serverIdentity kh <- asks serverIdentity
liftIO (runExceptT $ serverHandshake h kh) >>= \case liftIO (runExceptT $ serverHandshake h kh) >>= \case
@@ -157,7 +139,7 @@ clientDisconnected c@Client {subscriptions, connected} = do
subs <- readTVarIO subscriptions subs <- readTVarIO subscriptions
mapM_ cancelSub subs mapM_ cancelSub subs
cs <- asks $ subscribers . server cs <- asks $ subscribers . server
atomically . mapM_ (\rId -> TM.update deleteCurrentClient rId cs) $ M.keys subs atomically . mapM_ (modifyTVar cs . M.update deleteCurrentClient) $ M.keys subs
where where
deleteCurrentClient :: Client -> Maybe Client deleteCurrentClient :: Client -> Maybe Client
deleteCurrentClient c' deleteCurrentClient c'
@@ -248,11 +230,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
Cmd SNotifier NSUB -> subscribeNotifications Cmd SNotifier NSUB -> subscribeNotifications
Cmd SRecipient command -> Cmd SRecipient command ->
case command of case command of
NEW rKey dhKey -> NEW rKey dhKey -> createQueue st rKey dhKey
ifM
(asks $ allowNewQueues . config)
(createQueue st rKey dhKey)
(pure (corrId, queueId, ERR AUTH))
SUB -> subscribeQueue queueId SUB -> subscribeQueue queueId
ACK -> acknowledgeMsg ACK -> acknowledgeMsg
KEY sKey -> secureQueue_ st sKey KEY sKey -> secureQueue_ st sKey
@@ -331,19 +309,21 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
getSubscription :: RecipientId -> STM Sub getSubscription :: RecipientId -> STM Sub
getSubscription rId = do getSubscription rId = do
TM.lookup rId subscriptions >>= \case subs <- readTVar subscriptions
case M.lookup rId subs of
Just s -> tryTakeTMVar (delivered s) $> s Just s -> tryTakeTMVar (delivered s) $> s
Nothing -> do Nothing -> do
writeTBQueue subscribedQ (rId, clnt) writeTBQueue subscribedQ (rId, clnt)
s <- newSubscription s <- newSubscription
TM.insert rId s subscriptions writeTVar subscriptions $ M.insert rId s subs
return s return s
subscribeNotifications :: m (Transmission BrokerMsg) subscribeNotifications :: m (Transmission BrokerMsg)
subscribeNotifications = atomically $ do subscribeNotifications = atomically $ do
whenM (isNothing <$> TM.lookup queueId ntfSubscriptions) $ do subs <- readTVar ntfSubscriptions
when (isNothing $ M.lookup queueId subs) $ do
writeTBQueue ntfSubscribedQ (queueId, clnt) writeTBQueue ntfSubscribedQ (queueId, clnt)
TM.insert queueId () ntfSubscriptions writeTVar ntfSubscriptions $ M.insert queueId () subs
pure ok pure ok
acknowledgeMsg :: m (Transmission BrokerMsg) acknowledgeMsg :: m (Transmission BrokerMsg)
@@ -354,7 +334,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
_ -> return $ err NO_MSG _ -> return $ err NO_MSG
withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a) withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a)
withSub rId f = mapM f =<< TM.lookup rId subscriptions withSub rId f = readTVar subscriptions >>= mapM f . M.lookup rId
sendMessage :: QueueStore -> MsgBody -> m (Transmission BrokerMsg) sendMessage :: QueueStore -> MsgBody -> m (Transmission BrokerMsg)
sendMessage st msgBody sendMessage st msgBody
@@ -371,11 +351,9 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
Left _ -> pure $ err LARGE_MSG Left _ -> pure $ err LARGE_MSG
Right msg -> do Right msg -> do
ms <- asks msgStore ms <- asks msgStore
ServerConfig {messageTTL, msgQueueQuota} <- asks config quota <- asks $ msgQueueQuota . config
old <- forM messageTTL $ \ttl -> subtract ttl . systemSeconds <$> liftIO getSystemTime
atomically $ do atomically $ do
q <- getMsgQueue ms (recipientId qr) msgQueueQuota q <- getMsgQueue ms (recipientId qr) quota
mapM_ (deleteExpiredMsgs q) old
ifM (isFull q) (pure $ err QUOTA) $ do ifM (isFull q) (pure $ err QUOTA) $ do
trySendNotification trySendNotification
writeMsg q msg writeMsg q msg
@@ -391,7 +369,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
trySendNotification :: STM () trySendNotification :: STM ()
trySendNotification = trySendNotification =
forM_ (notifier qr) $ \(nId, _) -> forM_ (notifier qr) $ \(nId, _) ->
mapM_ (writeNtf nId) =<< TM.lookup nId notifiers mapM_ (writeNtf nId) . M.lookup nId =<< readTVar notifiers
writeNtf :: NotifierId -> Client -> STM () writeNtf :: NotifierId -> Client -> STM ()
writeNtf nId Client {sndQ = q} = writeNtf nId Client {sndQ = q} =
@@ -425,7 +403,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscri
void setDelivered void setDelivered
setSub :: (Sub -> Sub) -> STM () setSub :: (Sub -> Sub) -> STM ()
setSub f = TM.adjust f rId subscriptions setSub f = modifyTVar subscriptions $ M.adjust f rId
setDelivered :: STM (Maybe Bool) setDelivered :: STM (Maybe Bool)
setDelivered = withSub rId $ \s -> tryPutTMVar (delivered s) () setDelivered = withSub rId $ \s -> tryPutTMVar (delivered s) ()
+17 -23
View File
@@ -9,7 +9,6 @@ import Control.Concurrent (ThreadId)
import Control.Monad.IO.Unlift import Control.Monad.IO.Unlift
import Crypto.Random import Crypto.Random
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.X509.Validation (Fingerprint (..)) import Data.X509.Validation (Fingerprint (..))
@@ -22,8 +21,6 @@ import Simplex.Messaging.Server.MsgStore.STM
import Simplex.Messaging.Server.QueueStore (QueueRec (..)) import Simplex.Messaging.Server.QueueStore (QueueRec (..))
import Simplex.Messaging.Server.QueueStore.STM import Simplex.Messaging.Server.QueueStore.STM
import Simplex.Messaging.Server.StoreLog 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 (ATransport)
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams) import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams)
import System.IO (IOMode (..)) import System.IO (IOMode (..))
@@ -37,12 +34,6 @@ data ServerConfig = ServerConfig
queueIdBytes :: Int, queueIdBytes :: Int,
msgIdBytes :: Int, msgIdBytes :: Int,
storeLog :: Maybe (StoreLog 'ReadMode), storeLog :: Maybe (StoreLog 'ReadMode),
-- | set to False to prohibit creating new queues
allowNewQueues :: Bool,
-- | time after which the messages can be removed from the queues, seconds
messageTTL :: Maybe Int64,
-- | interval to periodically remove expired messages (when no messages are sent to the queue), microseconds
expireMessagesInterval :: Maybe Int,
-- CA certificate private key is not needed for initialization -- CA certificate private key is not needed for initialization
caCertificateFile :: FilePath, caCertificateFile :: FilePath,
privateKeyFile :: FilePath, privateKeyFile :: FilePath,
@@ -62,14 +53,14 @@ data Env = Env
data Server = Server data Server = Server
{ subscribedQ :: TBQueue (RecipientId, Client), { subscribedQ :: TBQueue (RecipientId, Client),
subscribers :: TMap RecipientId Client, subscribers :: TVar (Map RecipientId Client),
ntfSubscribedQ :: TBQueue (NotifierId, Client), ntfSubscribedQ :: TBQueue (NotifierId, Client),
notifiers :: TMap NotifierId Client notifiers :: TVar (Map NotifierId Client)
} }
data Client = Client data Client = Client
{ subscriptions :: TMap RecipientId Sub, { subscriptions :: TVar (Map RecipientId Sub),
ntfSubscriptions :: TMap NotifierId (), ntfSubscriptions :: TVar (Map NotifierId ()),
rcvQ :: TBQueue (Transmission Cmd), rcvQ :: TBQueue (Transmission Cmd),
sndQ :: TBQueue (Transmission BrokerMsg), sndQ :: TBQueue (Transmission BrokerMsg),
sessionId :: ByteString, sessionId :: ByteString,
@@ -86,15 +77,15 @@ data Sub = Sub
newServer :: Natural -> STM Server newServer :: Natural -> STM Server
newServer qSize = do newServer qSize = do
subscribedQ <- newTBQueue qSize subscribedQ <- newTBQueue qSize
subscribers <- TM.empty subscribers <- newTVar M.empty
ntfSubscribedQ <- newTBQueue qSize ntfSubscribedQ <- newTBQueue qSize
notifiers <- TM.empty notifiers <- newTVar M.empty
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers} return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers}
newClient :: Natural -> ByteString -> STM Client newClient :: Natural -> ByteString -> STM Client
newClient qSize sessionId = do newClient qSize sessionId = do
subscriptions <- TM.empty subscriptions <- newTVar M.empty
ntfSubscriptions <- TM.empty ntfSubscriptions <- newTVar M.empty
rcvQ <- newTBQueue qSize rcvQ <- newTBQueue qSize
sndQ <- newTBQueue qSize sndQ <- newTBQueue qSize
connected <- newTVar True connected <- newTVar True
@@ -118,12 +109,15 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile}
return Env {config, server, serverIdentity, queueStore, msgStore, idsDrg, storeLog = s', tlsServerParams} return Env {config, server, serverIdentity, queueStore, msgStore, idsDrg, storeLog = s', tlsServerParams}
where where
restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode) restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode)
restoreQueues QueueStore {queues, senders, notifiers} s = do restoreQueues queueStore s = do
(qs, s') <- liftIO $ readWriteStoreLog s (queues, s') <- liftIO $ readWriteStoreLog s
atomically $ do atomically $
writeTVar queues =<< mapM newTVar qs modifyTVar queueStore $ \d ->
writeTVar senders $ M.foldr' addSender M.empty qs d
writeTVar notifiers $ M.foldr' addNotifier M.empty qs { queues,
senders = M.foldr' addSender M.empty queues,
notifiers = M.foldr' addNotifier M.empty queues
}
pure s' pure s'
addSender :: QueueRec -> Map SenderId RecipientId -> Map SenderId RecipientId addSender :: QueueRec -> Map SenderId RecipientId -> Map SenderId RecipientId
addSender q = M.insert (senderId q) (recipientId q) addSender q = M.insert (senderId q) (recipientId q)
-2
View File
@@ -2,7 +2,6 @@
module Simplex.Messaging.Server.MsgStore where module Simplex.Messaging.Server.MsgStore where
import Data.Int (Int64)
import Data.Time.Clock.System (SystemTime) import Data.Time.Clock.System (SystemTime)
import Numeric.Natural import Numeric.Natural
import Simplex.Messaging.Protocol (MsgBody, MsgId, RecipientId) import Simplex.Messaging.Protocol (MsgBody, MsgId, RecipientId)
@@ -23,4 +22,3 @@ class MonadMsgQueue q m where
tryPeekMsg :: q -> m (Maybe Message) -- non blocking tryPeekMsg :: q -> m (Maybe Message) -- non blocking
peekMsg :: q -> m Message -- blocking peekMsg :: q -> m Message -- blocking
tryDelPeekMsg :: q -> m (Maybe Message) -- atomic delete (== read) last and peek next message, if available tryDelPeekMsg :: q -> m (Maybe Message) -- atomic delete (== read) last and peek next message, if available
deleteExpiredMsgs :: q -> Int64 -> m ()
+13 -20
View File
@@ -3,38 +3,39 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Messaging.Server.MsgStore.STM where module Simplex.Messaging.Server.MsgStore.STM where
import Control.Monad (when) import Data.Map.Strict (Map)
import Data.Int (Int64) import qualified Data.Map.Strict as M
import Data.Time.Clock.System (SystemTime (systemSeconds))
import Numeric.Natural import Numeric.Natural
import Simplex.Messaging.Protocol (RecipientId) import Simplex.Messaging.Protocol (RecipientId)
import Simplex.Messaging.Server.MsgStore import Simplex.Messaging.Server.MsgStore
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import UnliftIO.STM import UnliftIO.STM
newtype MsgQueue = MsgQueue {msgQueue :: TBQueue Message} newtype MsgQueue = MsgQueue {msgQueue :: TBQueue Message}
type STMMsgStore = TMap RecipientId MsgQueue newtype MsgStoreData = MsgStoreData {messages :: Map RecipientId MsgQueue}
type STMMsgStore = TVar MsgStoreData
newMsgStore :: STM STMMsgStore newMsgStore :: STM STMMsgStore
newMsgStore = TM.empty newMsgStore = newTVar $ MsgStoreData M.empty
instance MonadMsgStore STMMsgStore MsgQueue STM where instance MonadMsgStore STMMsgStore MsgQueue STM where
getMsgQueue :: STMMsgStore -> RecipientId -> Natural -> STM MsgQueue getMsgQueue :: STMMsgStore -> RecipientId -> Natural -> STM MsgQueue
getMsgQueue st rId quota = maybe newQ pure =<< TM.lookup rId st getMsgQueue store rId quota = do
m <- messages <$> readTVar store
maybe (newQ m) return $ M.lookup rId m
where where
newQ = do newQ m' = do
q <- MsgQueue <$> newTBQueue quota q <- MsgQueue <$> newTBQueue quota
TM.insert rId q st writeTVar store . MsgStoreData $ M.insert rId q m'
return q return q
delMsgQueue :: STMMsgStore -> RecipientId -> STM () delMsgQueue :: STMMsgStore -> RecipientId -> STM ()
delMsgQueue st rId = TM.delete rId st delMsgQueue store rId =
modifyTVar store $ MsgStoreData . M.delete rId . messages
instance MonadMsgQueue MsgQueue STM where instance MonadMsgQueue MsgQueue STM where
isFull :: MsgQueue -> STM Bool isFull :: MsgQueue -> STM Bool
@@ -52,11 +53,3 @@ instance MonadMsgQueue MsgQueue STM where
-- atomic delete (== read) last and peek next message if available -- atomic delete (== read) last and peek next message if available
tryDelPeekMsg :: MsgQueue -> STM (Maybe Message) tryDelPeekMsg :: MsgQueue -> STM (Maybe Message)
tryDelPeekMsg (MsgQueue q) = tryReadTBQueue q >> tryPeekTBQueue q tryDelPeekMsg (MsgQueue q) = tryReadTBQueue q >> tryPeekTBQueue q
deleteExpiredMsgs :: MsgQueue -> Int64 -> STM ()
deleteExpiredMsgs (MsgQueue q) old = loop
where
loop = tryPeekTBQueue q >>= mapM_ delOldMsg
delOldMsg Message {ts} =
when (systemSeconds ts < old) $
tryReadTBQueue q >> loop
@@ -15,7 +15,6 @@ data QueueRec = QueueRec
notifier :: Maybe (NotifierId, NtfPublicVerifyKey), notifier :: Maybe (NotifierId, NtfPublicVerifyKey),
status :: QueueStatus status :: QueueStatus
} }
deriving (Eq, Show)
data QueueStatus = QueueActive | QueueOff deriving (Eq, Show) data QueueStatus = QueueActive | QueueOff deriving (Eq, Show)
+80 -57
View File
@@ -3,7 +3,6 @@
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE KindSignatures #-} {-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
@@ -12,83 +11,107 @@
module Simplex.Messaging.Server.QueueStore.STM where module Simplex.Messaging.Server.QueueStore.STM where
import Control.Monad import Data.Map.Strict (Map)
import Data.Functor (($>)) import qualified Data.Map.Strict as M
import Simplex.Messaging.Protocol import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.QueueStore import Simplex.Messaging.Server.QueueStore
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (ifM)
import UnliftIO.STM import UnliftIO.STM
data QueueStore = QueueStore data QueueStoreData = QueueStoreData
{ queues :: TMap RecipientId (TVar QueueRec), { queues :: Map RecipientId QueueRec,
senders :: TMap SenderId RecipientId, senders :: Map SenderId RecipientId,
notifiers :: TMap NotifierId RecipientId notifiers :: Map NotifierId RecipientId
} }
type QueueStore = TVar QueueStoreData
newQueueStore :: STM QueueStore newQueueStore :: STM QueueStore
newQueueStore = do newQueueStore = newTVar QueueStoreData {queues = M.empty, senders = M.empty, notifiers = M.empty}
queues <- TM.empty
senders <- TM.empty
notifiers <- TM.empty
pure QueueStore {queues, senders, notifiers}
instance MonadQueueStore QueueStore STM where instance MonadQueueStore QueueStore STM where
addQueue :: QueueStore -> QueueRec -> STM (Either ErrorType ()) addQueue :: QueueStore -> QueueRec -> STM (Either ErrorType ())
addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = sId} = do addQueue store qRec@QueueRec {recipientId = rId, senderId = sId} = do
ifM hasId (pure $ Left DUPLICATE_) $ do cs@QueueStoreData {queues, senders} <- readTVar store
qVar <- newTVar q if M.member rId queues || M.member sId senders
TM.insert rId qVar queues then return $ Left DUPLICATE_
TM.insert sId rId senders else do
pure $ Right () writeTVar store $
where cs
hasId = (||) <$> TM.member rId queues <*> TM.member sId senders { queues = M.insert rId qRec queues,
senders = M.insert sId rId senders
}
return $ Right ()
getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
getQueue QueueStore {queues, senders, notifiers} party qId = getQueue st party qId = do
toResult <$> (mapM readTVar =<< getVar) cs <- readTVar st
pure $ case party of
SRecipient -> getRcpQueue cs qId
SSender -> getPartyQueue cs senders
SNotifier -> getPartyQueue cs notifiers
where where
getVar = case party of getPartyQueue ::
SRecipient -> TM.lookup qId queues QueueStoreData ->
SSender -> TM.lookup qId senders >>= get (QueueStoreData -> Map QueueId RecipientId) ->
SNotifier -> TM.lookup qId notifiers >>= get Either ErrorType QueueRec
get = fmap join . mapM (`TM.lookup` queues) getPartyQueue cs recipientIds =
case M.lookup qId $ recipientIds cs of
Just rId -> getRcpQueue cs rId
Nothing -> Left AUTH
secureQueue :: QueueStore -> RecipientId -> SndPublicVerifyKey -> STM (Either ErrorType QueueRec) secureQueue :: QueueStore -> RecipientId -> SndPublicVerifyKey -> STM (Either ErrorType QueueRec)
secureQueue QueueStore {queues} rId sKey = secureQueue store rId sKey =
withQueue rId queues $ \qVar -> updateQueues store rId $ \cs c ->
readTVar qVar >>= \q -> case senderKey q of case senderKey c of
Just _ -> pure Nothing Just _ -> (Left AUTH, cs)
_ -> writeTVar qVar q {senderKey = Just sKey} $> Just q _ -> (Right c, cs {queues = M.insert rId c {senderKey = Just sKey} (queues cs)})
addQueueNotifier :: QueueStore -> RecipientId -> NotifierId -> NtfPublicVerifyKey -> STM (Either ErrorType QueueRec) addQueueNotifier :: QueueStore -> RecipientId -> NotifierId -> NtfPublicVerifyKey -> STM (Either ErrorType QueueRec)
addQueueNotifier QueueStore {queues, notifiers} rId nId nKey = do addQueueNotifier store rId nId nKey = do
ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $ cs@QueueStoreData {queues, notifiers} <- readTVar store
withQueue rId queues $ \qVar -> if M.member nId notifiers
readTVar qVar >>= \q -> case notifier q of then pure $ Left DUPLICATE_
Just _ -> pure Nothing else case M.lookup rId queues of
Nothing -> pure $ Left AUTH
Just q -> case notifier q of
Just _ -> pure $ Left AUTH
_ -> do _ -> do
writeTVar qVar q {notifier = Just (nId, nKey)} writeTVar store $
TM.insert nId rId notifiers cs
pure $ Just q { queues = M.insert rId q {notifier = Just (nId, nKey)} queues,
notifiers = M.insert nId rId notifiers
}
pure $ Right q
suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ()) suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
suspendQueue QueueStore {queues} rId = suspendQueue store rId =
withQueue rId queues $ \qVar -> modifyTVar' qVar (\q -> q {status = QueueOff}) $> Just () updateQueues store rId $ \cs c ->
(Right (), cs {queues = M.insert rId c {status = QueueOff} (queues cs)})
deleteQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ()) deleteQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
deleteQueue QueueStore {queues, senders, notifiers} rId = do deleteQueue store rId =
TM.lookupDelete rId queues >>= \case updateQueues store rId $ \cs c ->
Just qVar -> ( Right (),
readTVar qVar >>= \q -> do cs
TM.delete (senderId q) senders { queues = M.delete rId (queues cs),
forM_ (notifier q) $ \(nId, _) -> TM.delete nId notifiers senders = M.delete (senderId c) (senders cs)
pure $ Right () }
_ -> pure $ Left AUTH )
toResult :: Maybe a -> Either ErrorType a updateQueues ::
toResult = maybe (Left AUTH) Right QueueStore ->
RecipientId ->
(QueueStoreData -> QueueRec -> (Either ErrorType a, QueueStoreData)) ->
STM (Either ErrorType a)
updateQueues store rId update = do
cs <- readTVar store
let conn = getRcpQueue cs rId
either (return . Left) (_update cs) conn
where
_update cs c = do
let (res, cs') = update cs c
writeTVar store cs'
return res
withQueue :: RecipientId -> TMap RecipientId (TVar QueueRec) -> (TVar QueueRec -> STM (Maybe a)) -> STM (Either ErrorType a) getRcpQueue :: QueueStoreData -> RecipientId -> Either ErrorType QueueRec
withQueue rId queues f = toResult <$> (TM.lookup rId queues >>= fmap join . mapM f) getRcpQueue cs rId = maybe (Left AUTH) Right . M.lookup rId $ queues cs
-70
View File
@@ -1,70 +0,0 @@
module Simplex.Messaging.TMap
( TMap,
empty,
singleton,
Simplex.Messaging.TMap.lookup,
member,
insert,
delete,
lookupInsert,
lookupDelete,
adjust,
update,
alter,
union,
)
where
import Control.Concurrent.STM
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
type TMap k a = TVar (Map k a)
empty :: STM (TMap k a)
empty = newTVar M.empty
{-# INLINE empty #-}
singleton :: k -> a -> STM (TMap k a)
singleton k v = newTVar $ M.singleton k v
{-# INLINE singleton #-}
lookup :: Ord k => k -> TMap k a -> STM (Maybe a)
lookup k m = M.lookup k <$> readTVar m
{-# INLINE lookup #-}
member :: Ord k => k -> TMap k a -> STM Bool
member k m = M.member k <$> readTVar m
{-# INLINE member #-}
insert :: Ord k => k -> a -> TMap k a -> STM ()
insert k v m = modifyTVar' m $ M.insert k v
{-# INLINE insert #-}
delete :: Ord k => k -> TMap k a -> STM ()
delete k m = modifyTVar' m $ M.delete k
{-# INLINE delete #-}
lookupInsert :: Ord k => k -> a -> TMap k a -> STM (Maybe a)
lookupInsert k v m = stateTVar m $ \mv -> (M.lookup k mv, M.insert k v mv)
{-# INLINE lookupInsert #-}
lookupDelete :: Ord k => k -> TMap k a -> STM (Maybe a)
lookupDelete k m = stateTVar m $ \mv -> (M.lookup k mv, M.delete k mv)
{-# INLINE lookupDelete #-}
adjust :: Ord k => (a -> a) -> k -> TMap k a -> STM ()
adjust f k m = modifyTVar' m $ M.adjust f k
{-# INLINE adjust #-}
update :: Ord k => (a -> Maybe a) -> k -> TMap k a -> STM ()
update f k m = modifyTVar' m $ M.update f k
{-# INLINE update #-}
alter :: Ord k => (Maybe a -> Maybe a) -> k -> TMap k a -> STM ()
alter f k m = modifyTVar' m $ M.alter f k
{-# INLINE alter #-}
union :: Ord k => Map k a -> TMap k a -> STM ()
union m' m = modifyTVar' m $ M.union m'
{-# INLINE union #-}
+4 -8
View File
@@ -38,7 +38,6 @@ module Simplex.Messaging.Transport
-- * TLS Transport -- * TLS Transport
TLS (..), TLS (..),
SessionId,
connectTLS, connectTLS,
closeTLS, closeTLS,
supportedParameters, supportedParameters,
@@ -97,7 +96,7 @@ supportedSMPVersions :: VersionRange
supportedSMPVersions = mkVersionRange 1 1 supportedSMPVersions = mkVersionRange 1 1
simplexMQVersion :: String simplexMQVersion :: String
simplexMQVersion = "1.1.0" simplexMQVersion = "1.0.2"
-- * Transport connection class -- * Transport connection class
@@ -116,7 +115,7 @@ class Transport c where
getClientConnection :: T.Context -> IO c getClientConnection :: T.Context -> IO c
-- | tls-unique channel binding per RFC5929 -- | tls-unique channel binding per RFC5929
tlsUnique :: c -> SessionId tlsUnique :: c -> ByteString
-- | Close connection -- | Close connection
closeConnection :: c -> IO () closeConnection :: c -> IO ()
@@ -251,17 +250,14 @@ trimCR s = if B.last s == '\r' then B.init s else s
-- | The handle for SMP encrypted transport connection over Transport . -- | The handle for SMP encrypted transport connection over Transport .
data THandle c = THandle data THandle c = THandle
{ connection :: c, { connection :: c,
sessionId :: SessionId, sessionId :: ByteString,
-- | agreed SMP server protocol version -- | agreed SMP server protocol version
smpVersion :: Version smpVersion :: Version
} }
-- | TLS-unique channel binding
type SessionId = ByteString
data ServerHandshake = ServerHandshake data ServerHandshake = ServerHandshake
{ smpVersionRange :: VersionRange, { smpVersionRange :: VersionRange,
sessionId :: SessionId sessionId :: ByteString
} }
data ClientHandshake = ClientHandshake data ClientHandshake = ClientHandshake
+5 -7
View File
@@ -20,20 +20,19 @@ import Network.Socket
import qualified Network.TLS as T import qualified Network.TLS as T
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Transport import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.KeepAlive
import System.IO.Error import System.IO.Error
import UnliftIO.Exception (IOException) import UnliftIO.Exception (IOException)
import qualified UnliftIO.Exception as E import qualified UnliftIO.Exception as E
-- | Connect to passed TCP host:port and pass handle to the client. -- | Connect to passed TCP host:port and pass handle to the client.
runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> C.KeyHash -> Maybe KeepAliveOpts -> (c -> m a) -> m a runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> C.KeyHash -> (c -> m a) -> m a
runTransportClient host port keyHash keepAliveOpts client = do runTransportClient host port keyHash client = do
let clientParams = mkTLSClientParams host port keyHash let clientParams = mkTLSClientParams host port keyHash
c <- liftIO $ startTCPClient host port clientParams keepAliveOpts c <- liftIO $ startTCPClient host port clientParams
client c `E.finally` liftIO (closeConnection c) client c `E.finally` liftIO (closeConnection c)
startTCPClient :: forall c. Transport c => HostName -> ServiceName -> T.ClientParams -> Maybe KeepAliveOpts -> IO c startTCPClient :: forall c. Transport c => HostName -> ServiceName -> T.ClientParams -> IO c
startTCPClient host port clientParams keepAliveOpts = withSocketsDo $ resolve >>= tryOpen err startTCPClient host port clientParams = withSocketsDo $ resolve >>= tryOpen err
where where
err :: IOException err :: IOException
err = mkIOError NoSuchThing "no address" Nothing Nothing err = mkIOError NoSuchThing "no address" Nothing Nothing
@@ -52,7 +51,6 @@ startTCPClient host port clientParams keepAliveOpts = withSocketsDo $ resolve >>
open addr = do open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
connect sock $ addrAddress addr connect sock $ addrAddress addr
mapM_ (setSocketKeepAlive sock) keepAliveOpts
ctx <- connectTLS clientParams sock ctx <- connectTLS clientParams sock
getClientConnection ctx getClientConnection ctx
@@ -1,62 +0,0 @@
{-# LANGUAGE CApiFFI #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Messaging.Transport.KeepAlive where
import Foreign.C (CInt (..))
import Network.Socket
data KeepAliveOpts = KeepAliveOpts
{ keepIdle :: Int,
keepIntvl :: Int,
keepCnt :: Int
}
defaultKeepAliveOpts :: KeepAliveOpts
defaultKeepAliveOpts =
KeepAliveOpts
{ keepIdle = 30,
keepIntvl = 15,
keepCnt = 4
}
_SOL_TCP :: CInt
_SOL_TCP = 6
#if defined(mingw32_HOST_OS)
-- Windows
-- The values are copied from windows::Win32::Networking::WinSock
-- https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Networking/WinSock/index.html
_TCP_KEEPIDLE :: CInt
_TCP_KEEPIDLE = 3
_TCP_KEEPINTVL :: CInt
_TCP_KEEPINTVL = 17
_TCP_KEEPCNT :: CInt
_TCP_KEEPCNT = 16
#else
-- Mac/Linux
#if defined(darwin_HOST_OS)
foreign import capi "netinet/tcp.h value TCP_KEEPALIVE" _TCP_KEEPIDLE :: CInt
#else
foreign import capi "netinet/tcp.h value TCP_KEEPIDLE" _TCP_KEEPIDLE :: CInt
#endif
foreign import capi "netinet/tcp.h value TCP_KEEPINTVL" _TCP_KEEPINTVL :: CInt
foreign import capi "netinet/tcp.h value TCP_KEEPCNT" _TCP_KEEPCNT :: CInt
#endif
setSocketKeepAlive :: Socket -> KeepAliveOpts -> IO ()
setSocketKeepAlive sock KeepAliveOpts {keepCnt, keepIdle, keepIntvl} = do
setSocketOption sock KeepAlive 1
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPIDLE) keepIdle
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPINTVL) keepIntvl
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPCNT) keepCnt
+7 -20
View File
@@ -4,7 +4,6 @@
module Simplex.Messaging.Transport.Server module Simplex.Messaging.Transport.Server
( runTransportServer, ( runTransportServer,
runTCPServer,
loadTLSServerParams, loadTLSServerParams,
loadFingerprint, loadFingerprint,
serverHandshake, serverHandshake,
@@ -38,11 +37,11 @@ runTransportServer started port serverParams server = do
clients <- newTVarIO S.empty clients <- newTVarIO S.empty
E.bracket E.bracket
(startTCPServer started port) (startTCPServer started port)
(closeServer started clients) (closeServer clients)
$ \sock -> forever $ do $ \sock -> forever $ do
(connSock, _) <- accept sock (connSock, _) <- accept sock
tid <- forkIO $ connectClient u connSock `E.catch` \(_ :: E.SomeException) -> pure () tid <- forkIO $ connectClient u connSock `E.catch` \(_ :: E.SomeException) -> pure ()
atomically . modifyTVar' clients $ S.insert tid atomically . modifyTVar clients $ S.insert tid
where where
connectClient :: UnliftIO m -> Socket -> IO () connectClient :: UnliftIO m -> Socket -> IO ()
connectClient u connSock = connectClient u connSock =
@@ -50,23 +49,11 @@ runTransportServer started port serverParams server = do
(connectTLS serverParams connSock >>= getServerConnection) (connectTLS serverParams connSock >>= getServerConnection)
closeConnection closeConnection
(unliftIO u . server) (unliftIO u . server)
closeServer :: TVar (Set ThreadId) -> Socket -> IO ()
runTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO () closeServer clients sock = do
runTCPServer started port server = do readTVarIO clients >>= mapM_ killThread
clients <- newTVarIO S.empty close sock
E.bracket void . atomically $ tryPutTMVar started False
(startTCPServer started port)
(closeServer started clients)
$ \sock -> forever $ do
(connSock, _) <- accept sock
tid <- forkIO $ server connSock `E.catch` \(_ :: E.SomeException) -> pure ()
atomically . modifyTVar' clients $ S.insert tid
closeServer :: TMVar Bool -> TVar (Set ThreadId) -> Socket -> IO ()
closeServer started clients sock = do
readTVarIO clients >>= mapM_ killThread
close sock
void . atomically $ tryPutTMVar started False
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
-4
View File
@@ -58,10 +58,6 @@ ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM ba t f = ba >>= \b -> if b then t else f ifM ba t f = ba >>= \b -> if b then t else f
{-# INLINE ifM #-} {-# INLINE ifM #-}
whenM :: Monad m => m Bool -> m () -> m ()
whenM b a = ifM b a $ pure ()
{-# INLINE whenM #-}
unlessM :: Monad m => m Bool -> m () -> m () unlessM :: Monad m => m Bool -> m () -> m ()
unlessM b = ifM b $ pure () unlessM b = ifM b $ pure ()
{-# INLINE unlessM #-} {-# INLINE unlessM #-}
-1
View File
@@ -36,7 +36,6 @@ packages:
# #
extra-deps: extra-deps:
- cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881 - cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881
- network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079 - simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
- tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991 - 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
+68 -104
View File
@@ -13,6 +13,7 @@ import AgentTests.ConnectionRequestTests
import AgentTests.DoubleRatchetTests (doubleRatchetTests) import AgentTests.DoubleRatchetTests (doubleRatchetTests)
import AgentTests.FunctionalAPITests (functionalAPITests) import AgentTests.FunctionalAPITests (functionalAPITests)
import AgentTests.SQLiteTests (storeTests) import AgentTests.SQLiteTests (storeTests)
import AgentTests.PostgresTests (postgresStoreTests)
import Control.Concurrent import Control.Concurrent
import Control.Monad (forM_) import Control.Monad (forM_)
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
@@ -36,6 +37,7 @@ agentTests (ATransport t) = do
describe "Double ratchet tests" doubleRatchetTests describe "Double ratchet tests" doubleRatchetTests
describe "Functional API" $ functionalAPITests (ATransport t) describe "Functional API" $ functionalAPITests (ATransport t)
describe "SQLite store" storeTests describe "SQLite store" storeTests
describe "Postgres store" postgresStoreTests
describe "SMP agent protocol syntax" $ syntaxTests t describe "SMP agent protocol syntax" $ syntaxTests t
describe "Establishing duplex connection" $ do describe "Establishing duplex connection" $ do
it "should connect via one server and one agent" $ it "should connect via one server and one agent" $
@@ -62,11 +64,9 @@ agentTests (ATransport t) = do
smpAgentTest3_1_1 $ testSubscription t smpAgentTest3_1_1 $ testSubscription t
it "should send notifications to client when server disconnects" $ it "should send notifications to client when server disconnects" $
smpAgentServerTest $ testSubscrNotification t smpAgentServerTest $ testSubscrNotification t
describe "Message delivery and server reconnection" $ do describe "Message delivery" $ do
it "should deliver messages after losing server connection and re-connecting" $ it "should deliver messages after losing server connection and re-connecting" $
smpAgentTest2_2_2_needs_server $ testMsgDeliveryServerRestart t smpAgentTest2_2_2_needs_server $ testMsgDeliveryServerRestart t
it "should connect to the server when server goes up if it initially was down" $
smpAgentTestN [] $ testServerConnectionAfterError t
it "should deliver pending messages after agent restarting" $ it "should deliver pending messages after agent restarting" $
smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t
it "should concurrently deliver messages to connections without blocking" $ it "should concurrently deliver messages to connections without blocking" $
@@ -128,25 +128,25 @@ testDuplexConnection _ alice bob = do
bob <# ("", "alice", CON) bob <# ("", "alice", CON)
alice <# ("", "bob", CON) alice <# ("", "bob", CON)
-- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4 -- 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 #: ("3", "bob", "SEND :hello") #> ("3", "bob", MID 4)
alice <# ("", "bob", SENT 5) alice <# ("", "bob", SENT 4)
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
bob #: ("12", "alice", "ACK 5") #> ("12", "alice", OK) bob #: ("12", "alice", "ACK 4") #> ("12", "alice", OK)
alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 6) alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 5)
alice <# ("", "bob", SENT 6) alice <# ("", "bob", SENT 5)
bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False
bob #: ("13", "alice", "ACK 6") #> ("13", "alice", OK) bob #: ("13", "alice", "ACK 5") #> ("13", "alice", OK)
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 7) bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 6)
bob <# ("", "alice", SENT 7) bob <# ("", "alice", SENT 6)
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
alice #: ("3a", "bob", "ACK 7") #> ("3a", "bob", OK) alice #: ("3a", "bob", "ACK 6") #> ("3a", "bob", OK)
bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 8) bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 7)
bob <# ("", "alice", SENT 8) bob <# ("", "alice", SENT 7)
alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False 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) alice #: ("5", "bob", "OFF") #> ("5", "bob", OK)
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 9) bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 8)
bob <# ("", "alice", MERR 9 (SMP AUTH)) bob <# ("", "alice", MERR 8 (SMP AUTH))
alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #: ("6", "bob", "DEL") #> ("6", "bob", OK)
alice #:# "nothing else should be delivered to alice" alice #:# "nothing else should be delivered to alice"
@@ -161,25 +161,25 @@ testDuplexConnRandomIds _ alice bob = do
bob <# ("", aliceConn, INFO "alice's connInfo") bob <# ("", aliceConn, INFO "alice's connInfo")
bob <# ("", aliceConn, CON) bob <# ("", aliceConn, CON)
alice <# ("", bobConn, CON) alice <# ("", bobConn, CON)
alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 5) alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 4)
alice <# ("", bobConn, SENT 5) alice <# ("", bobConn, SENT 4)
bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False
bob #: ("12", aliceConn, "ACK 5") #> ("12", aliceConn, OK) bob #: ("12", aliceConn, "ACK 4") #> ("12", aliceConn, OK)
alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 6) alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 5)
alice <# ("", bobConn, SENT 6) alice <# ("", bobConn, SENT 5)
bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False
bob #: ("13", aliceConn, "ACK 6") #> ("13", aliceConn, OK) bob #: ("13", aliceConn, "ACK 5") #> ("13", aliceConn, OK)
bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 7) bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 6)
bob <# ("", aliceConn, SENT 7) bob <# ("", aliceConn, SENT 6)
alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False
alice #: ("3a", bobConn, "ACK 7") #> ("3a", bobConn, OK) alice #: ("3a", bobConn, "ACK 6") #> ("3a", bobConn, OK)
bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 8) bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 7)
bob <# ("", aliceConn, SENT 8) bob <# ("", aliceConn, SENT 7)
alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False 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) alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK)
bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 9) bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 8)
bob <# ("", aliceConn, MERR 9 (SMP AUTH)) bob <# ("", aliceConn, MERR 8 (SMP AUTH))
alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK)
alice #:# "nothing else should be delivered to alice" alice #:# "nothing else should be delivered to alice"
@@ -196,10 +196,10 @@ testContactConnection _ alice bob tom = do
alice <# ("", "bob", INFO "bob's connInfo 2") alice <# ("", "bob", INFO "bob's connInfo 2")
alice <# ("", "bob", CON) alice <# ("", "bob", CON)
bob <# ("", "alice", CON) bob <# ("", "alice", CON)
alice #: ("3", "bob", "SEND :hi") #> ("3", "bob", MID 5) alice #: ("3", "bob", "SEND :hi") #> ("3", "bob", MID 4)
alice <# ("", "bob", SENT 5) alice <# ("", "bob", SENT 4)
bob <#= \case ("", "alice", Msg "hi") -> True; _ -> False 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) tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK)
("", "alice_contact", Right (REQ aInvId' "tom's connInfo")) <- (alice <#:) ("", "alice_contact", Right (REQ aInvId' "tom's connInfo")) <- (alice <#:)
@@ -209,10 +209,10 @@ testContactConnection _ alice bob tom = do
alice <# ("", "tom", INFO "tom's connInfo 2") alice <# ("", "tom", INFO "tom's connInfo 2")
alice <# ("", "tom", CON) alice <# ("", "tom", CON)
tom <# ("", "alice", CON) tom <# ("", "alice", CON)
alice #: ("5", "tom", "SEND :hi there") #> ("5", "tom", MID 5) alice #: ("5", "tom", "SEND :hi there") #> ("5", "tom", MID 4)
alice <# ("", "tom", SENT 5) alice <# ("", "tom", SENT 4)
tom <#= \case ("", "alice", Msg "hi there") -> True; _ -> False 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 :: Transport c => TProxy c -> c -> c -> IO ()
testContactConnRandomIds _ alice bob = do testContactConnRandomIds _ alice bob = do
@@ -232,10 +232,10 @@ testContactConnRandomIds _ alice bob = do
alice <# ("", bobConn, CON) alice <# ("", bobConn, CON)
bob <# ("", aliceConn, CON) bob <# ("", aliceConn, CON)
alice #: ("3", bobConn, "SEND :hi") #> ("3", bobConn, MID 5) alice #: ("3", bobConn, "SEND :hi") #> ("3", bobConn, MID 4)
alice <# ("", bobConn, SENT 5) alice <# ("", bobConn, SENT 4)
bob <#= \case ("", c, Msg "hi") -> c == aliceConn; _ -> False 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 :: Transport c => TProxy c -> c -> c -> IO ()
testRejectContactRequest _ alice bob = do testRejectContactRequest _ alice bob = do
@@ -252,20 +252,20 @@ testRejectContactRequest _ alice bob = do
testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO ()
testSubscription _ alice1 alice2 bob = do testSubscription _ alice1 alice2 bob = do
(alice1, "alice") `connect` (bob, "bob") (alice1, "alice") `connect` (bob, "bob")
bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 5) bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 4)
bob <# ("", "alice", SENT 5) bob <# ("", "alice", SENT 4)
alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False
alice1 #: ("1", "bob", "ACK 5") #> ("1", "bob", OK) alice1 #: ("1", "bob", "ACK 4") #> ("1", "bob", OK)
bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 6) bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 5)
bob <# ("", "alice", SENT 6) bob <# ("", "alice", SENT 5)
alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False 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) alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK)
alice1 <# ("", "bob", END) alice1 <# ("", "bob", END)
bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 7) bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 6)
bob <# ("", "alice", SENT 7) bob <# ("", "alice", SENT 6)
alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False 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" alice1 #:# "nothing else should be delivered to alice1"
testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO ()
@@ -281,77 +281,40 @@ testMsgDeliveryServerRestart :: Transport c => TProxy c -> c -> c -> IO ()
testMsgDeliveryServerRestart t alice bob = do testMsgDeliveryServerRestart t alice bob = do
withServer $ do withServer $ do
connect (alice, "alice") (bob, "bob") connect (alice, "alice") (bob, "bob")
bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 5) bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 4)
bob <# ("", "alice", SENT 5) bob <# ("", "alice", SENT 4)
alice <#= \case ("", "bob", Msg "hi") -> True; _ -> False 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" alice #:# "nothing else delivered before the server is killed"
alice <# ("", "bob", DOWN) alice <# ("", "bob", DOWN)
bob #: ("2", "alice", "SEND 11\nhello again") #> ("2", "alice", MID 6) bob #: ("2", "alice", "SEND 11\nhello again") #> ("2", "alice", MID 5)
bob #:# "nothing else delivered before the server is restarted" bob #:# "nothing else delivered before the server is restarted"
alice #:# "nothing else delivered before the server is restarted" alice #:# "nothing else delivered before the server is restarted"
withServer $ do withServer $ do
bob <# ("", "alice", SENT 6) bob <# ("", "alice", SENT 5)
alice <# ("", "bob", UP) alice <# ("", "bob", UP)
alice <#= \case ("", "bob", Msg "hello again") -> True; _ -> False 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 removeFile testStoreLogFile
where where
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` () withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
testServerConnectionAfterError :: forall c. Transport c => TProxy c -> [c] -> IO ()
testServerConnectionAfterError t _ = do
withAgent1 $ \bob -> do
withAgent2 $ \alice -> do
withServer $ do
connect (bob, "bob") (alice, "alice")
bob <# ("", "alice", DOWN)
alice <# ("", "bob", DOWN)
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 5)
alice #:# "nothing else delivered before the server is restarted"
bob #:# "nothing else delivered before the server is restarted"
withAgent1 $ \bob -> do
withAgent2 $ \alice -> do
bob #: ("1", "alice", "SUB") #> ("1", "alice", ERR (BROKER NETWORK))
alice #: ("1", "bob", "SUB") #> ("1", "bob", ERR (BROKER NETWORK))
withServer $ do
alice <#= \case ("", "bob", cmd) -> cmd == UP || cmd == SENT 5; _ -> False
alice <#= \case ("", "bob", cmd) -> cmd == UP || cmd == SENT 5; _ -> False
bob <# ("", "alice", UP)
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
bob #: ("2", "alice", "ACK 5") #> ("2", "alice", OK)
alice #: ("1", "bob", "SEND 11\nhello again") #> ("1", "bob", MID 6)
alice <# ("", "bob", SENT 6)
bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False
removeFile testStoreLogFile
removeFile testDB
removeFile testDB2
where
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
withAgent1 = withAgent agentTestPort testDB
withAgent2 = withAgent agentTestPort2 testDB2
withAgent :: String -> String -> (c -> IO a) -> IO a
withAgent agentPort agentDB = withSmpAgentThreadOn_ (ATransport t) (agentPort, testPort2, agentDB) (pure ()) . const . testSMPAgentClientOn agentPort
testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO () testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO ()
testMsgDeliveryAgentRestart t bob = do testMsgDeliveryAgentRestart t bob = do
withAgent $ \alice -> do withAgent $ \alice -> do
withServer $ do withServer $ do
connect (bob, "bob") (alice, "alice") connect (bob, "bob") (alice, "alice")
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 5) alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 4)
alice <# ("", "bob", SENT 5) alice <# ("", "bob", SENT 4)
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False 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 #:# "nothing else delivered before the server is down"
bob <# ("", "alice", DOWN) bob <# ("", "alice", DOWN)
alice #: ("2", "bob", "SEND 11\nhello again") #> ("2", "bob", MID 6) alice #: ("2", "bob", "SEND 11\nhello again") #> ("2", "bob", MID 5)
alice #:# "nothing else delivered before the server is restarted" alice #:# "nothing else delivered before the server is restarted"
bob #:# "nothing else delivered before the server is restarted" bob #:# "nothing else delivered before the server is restarted"
@@ -361,14 +324,14 @@ testMsgDeliveryAgentRestart t bob = do
alice <#= \case alice <#= \case
(corrId, "bob", cmd) -> (corrId, "bob", cmd) ->
(corrId == "3" && cmd == OK) (corrId == "3" && cmd == OK)
|| (corrId == "" && cmd == SENT 6) || (corrId == "" && cmd == SENT 5)
_ -> False _ -> False
bob <# ("", "alice", UP) bob <# ("", "alice", UP)
bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False 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 testStoreLogFile
removeFile testDB -- removeFile testDB
where where
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` () withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) (pure ()) . const . testSMPAgentClientOn agentTestPort withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) (pure ()) . const . testSMPAgentClientOn agentTestPort
@@ -393,11 +356,11 @@ testConcurrentMsgDelivery _ alice bob = do
-- alice <# ("", "bob", SENT 1) -- alice <# ("", "bob", SENT 1)
-- bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False -- bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
-- bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK) -- bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK)
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 6) bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 5)
bob <# ("", "alice", SENT 6) bob <# ("", "alice", SENT 5)
-- if delivery is blocked it won't go further -- if delivery is blocked it won't go further
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False 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 :: Transport c => TProxy c -> c -> c -> IO ()
testMsgDeliveryQuotaExceeded _ alice bob = do testMsgDeliveryQuotaExceeded _ alice bob = do
@@ -410,9 +373,9 @@ testMsgDeliveryQuotaExceeded _ alice bob = do
alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False
(_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND :over quota") (_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND :over quota")
alice #: ("1", "bob2", "SEND :hello") #> ("1", "bob2", MID 5) alice #: ("1", "bob2", "SEND :hello") #> ("1", "bob2", MID 4)
-- if delivery is blocked it won't go further -- 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 :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO ()
connect (h1, name1) (h2, name2) = do connect (h1, name1) (h2, name2) = do
@@ -461,6 +424,7 @@ syntaxTests t = do
-- TODO: add tests with defined connection id -- TODO: add tests with defined connection id
it "with incorrect parameter" $ ("222", "", "NEW hi") >#> ("222", "", "ERR CMD SYNTAX") it "with incorrect parameter" $ ("222", "", "NEW hi") >#> ("222", "", "ERR CMD SYNTAX")
-- focus this test to test postgres
describe "JOIN" $ do describe "JOIN" $ do
describe "valid" $ do describe "valid" $ do
it "using same server as in invitation" $ it "using same server as in invitation" $
+28 -54
View File
@@ -10,9 +10,9 @@ module AgentTests.FunctionalAPITests (functionalAPITests) where
import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.IO.Unlift import Control.Monad.IO.Unlift
import SMPAgentClient import SMPAgentClient
import SMPClient (testPort, withSmpServer, withSmpServerStoreLogOn) import SMPClient (withSmpServer)
import Simplex.Messaging.Agent import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..)) import Simplex.Messaging.Agent.Env.Postgres (AgentConfig (..))
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
import Simplex.Messaging.Transport (ATransport (..)) import Simplex.Messaging.Transport (ATransport (..))
@@ -44,15 +44,13 @@ functionalAPITests t = do
withSmpServer t testAsyncJoiningOfflineBeforeActivation withSmpServer t testAsyncJoiningOfflineBeforeActivation
it "should connect with both clients going offline" $ it "should connect with both clients going offline" $
withSmpServer t testAsyncBothOffline withSmpServer t testAsyncBothOffline
it "should connect on the second attempt if server was offline" $
testAsyncServerOffline t
it "should notify after HELLO timeout" $ it "should notify after HELLO timeout" $
withSmpServer t testAsyncHelloTimeout withSmpServer t testAsyncHelloTimeout
testAgentClient :: IO () testAgentClient :: IO ()
testAgentClient = do testAgentClient = do
alice <- getSMPAgentClient cfg alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2} bob <- getSMPAgentClient cfg {dbConnInfo = testDB2}
Right () <- runExceptT $ do Right () <- runExceptT $ do
(bobId, qInfo) <- createConnection alice SCMInvitation (bobId, qInfo) <- createConnection alice SCMInvitation
aliceId <- joinConnection bob qInfo "bob's connInfo" aliceId <- joinConnection bob qInfo "bob's connInfo"
@@ -61,26 +59,26 @@ testAgentClient = do
get alice ##> ("", bobId, CON) get alice ##> ("", bobId, CON)
get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, INFO "alice's connInfo")
get bob ##> ("", aliceId, CON) get bob ##> ("", aliceId, CON)
-- message IDs 1 to 4 get assigned to control messages, so first MSG is assigned ID 5 -- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4
5 <- sendMessage alice bobId "hello" 4 <- sendMessage alice bobId "hello"
get alice ##> ("", bobId, SENT 4)
5 <- sendMessage alice bobId "how are you?"
get alice ##> ("", bobId, SENT 5) get alice ##> ("", bobId, SENT 5)
6 <- sendMessage alice bobId "how are you?"
get alice ##> ("", bobId, SENT 6)
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
ackMessage bob aliceId 5 ackMessage bob aliceId 4
get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False
ackMessage bob aliceId 6 ackMessage bob aliceId 5
7 <- sendMessage bob aliceId "hello too" 6 <- sendMessage bob aliceId "hello too"
get bob ##> ("", aliceId, SENT 6)
7 <- sendMessage bob aliceId "message 1"
get bob ##> ("", aliceId, SENT 7) get bob ##> ("", aliceId, SENT 7)
8 <- sendMessage bob aliceId "message 1"
get bob ##> ("", aliceId, SENT 8)
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
ackMessage alice bobId 7 ackMessage alice bobId 6
get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False
ackMessage alice bobId 8 ackMessage alice bobId 7
suspendConnection alice bobId suspendConnection alice bobId
9 <- sendMessage bob aliceId "message 2" 8 <- sendMessage bob aliceId "message 2"
get bob ##> ("", aliceId, MERR 9 (SMP AUTH)) get bob ##> ("", aliceId, MERR 8 (SMP AUTH))
deleteConnection alice bobId deleteConnection alice bobId
liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages alice "nothing else should be delivered to alice"
pure () pure ()
@@ -96,7 +94,7 @@ testAgentClient = do
testAsyncInitiatingOffline :: IO () testAsyncInitiatingOffline :: IO ()
testAsyncInitiatingOffline = do testAsyncInitiatingOffline = do
alice <- getSMPAgentClient cfg alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2} bob <- getSMPAgentClient cfg {dbConnInfo = testDB2}
Right () <- runExceptT $ do Right () <- runExceptT $ do
(bobId, cReq) <- createConnection alice SCMInvitation (bobId, cReq) <- createConnection alice SCMInvitation
disconnectAgentClient alice disconnectAgentClient alice
@@ -114,14 +112,14 @@ testAsyncInitiatingOffline = do
testAsyncJoiningOfflineBeforeActivation :: IO () testAsyncJoiningOfflineBeforeActivation :: IO ()
testAsyncJoiningOfflineBeforeActivation = do testAsyncJoiningOfflineBeforeActivation = do
alice <- getSMPAgentClient cfg alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2} bob <- getSMPAgentClient cfg {dbConnInfo = testDB2}
Right () <- runExceptT $ do Right () <- runExceptT $ do
(bobId, qInfo) <- createConnection alice SCMInvitation (bobId, qInfo) <- createConnection alice SCMInvitation
aliceId <- joinConnection bob qInfo "bob's connInfo" aliceId <- joinConnection bob qInfo "bob's connInfo"
disconnectAgentClient bob disconnectAgentClient bob
("", _, CONF confId "bob's connInfo") <- get alice ("", _, CONF confId "bob's connInfo") <- get alice
allowConnection alice bobId confId "alice's connInfo" allowConnection alice bobId confId "alice's connInfo"
bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} bob' <- liftIO $ getSMPAgentClient cfg {dbConnInfo = testDB2}
subscribeConnection bob' aliceId subscribeConnection bob' aliceId
get alice ##> ("", bobId, CON) get alice ##> ("", bobId, CON)
get bob' ##> ("", aliceId, INFO "alice's connInfo") get bob' ##> ("", aliceId, INFO "alice's connInfo")
@@ -132,7 +130,7 @@ testAsyncJoiningOfflineBeforeActivation = do
testAsyncBothOffline :: IO () testAsyncBothOffline :: IO ()
testAsyncBothOffline = do testAsyncBothOffline = do
alice <- getSMPAgentClient cfg alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2} bob <- getSMPAgentClient cfg {dbConnInfo = testDB2}
Right () <- runExceptT $ do Right () <- runExceptT $ do
(bobId, cReq) <- createConnection alice SCMInvitation (bobId, cReq) <- createConnection alice SCMInvitation
disconnectAgentClient alice disconnectAgentClient alice
@@ -142,7 +140,7 @@ testAsyncBothOffline = do
subscribeConnection alice' bobId subscribeConnection alice' bobId
("", _, CONF confId "bob's connInfo") <- get alice' ("", _, CONF confId "bob's connInfo") <- get alice'
allowConnection alice' bobId confId "alice's connInfo" allowConnection alice' bobId confId "alice's connInfo"
bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} bob' <- liftIO $ getSMPAgentClient cfg {dbConnInfo = testDB2}
subscribeConnection bob' aliceId subscribeConnection bob' aliceId
get alice' ##> ("", bobId, CON) get alice' ##> ("", bobId, CON)
get bob' ##> ("", aliceId, INFO "alice's connInfo") get bob' ##> ("", aliceId, INFO "alice's connInfo")
@@ -150,34 +148,10 @@ testAsyncBothOffline = do
exchangeGreetings alice' bobId bob' aliceId exchangeGreetings alice' bobId bob' aliceId
pure () pure ()
testAsyncServerOffline :: ATransport -> IO ()
testAsyncServerOffline t = do
alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2}
-- create connection and shutdown the server
Right (bobId, cReq) <- withSmpServerStoreLogOn t testPort $ \_ ->
runExceptT $ createConnection alice SCMInvitation
-- connection fails
Left (BROKER NETWORK) <- runExceptT $ joinConnection bob cReq "bob's connInfo"
("", bobId1, DOWN) <- get alice
bobId1 `shouldBe` bobId
-- connection succeeds after server start
Right () <- withSmpServerStoreLogOn t testPort $ \_ -> runExceptT $ do
("", bobId2, UP) <- get alice
liftIO $ bobId2 `shouldBe` bobId
aliceId <- joinConnection bob cReq "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)
exchangeGreetings alice bobId bob aliceId
pure ()
testAsyncHelloTimeout :: IO () testAsyncHelloTimeout :: IO ()
testAsyncHelloTimeout = do testAsyncHelloTimeout = do
alice <- getSMPAgentClient cfg alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2, helloTimeout = 1} bob <- getSMPAgentClient cfg {dbConnInfo = testDB2, helloTimeout = 1}
Right () <- runExceptT $ do Right () <- runExceptT $ do
(_, cReq) <- createConnection alice SCMInvitation (_, cReq) <- createConnection alice SCMInvitation
disconnectAgentClient alice disconnectAgentClient alice
@@ -187,11 +161,11 @@ testAsyncHelloTimeout = do
exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
exchangeGreetings alice bobId bob aliceId = do exchangeGreetings alice bobId bob aliceId = do
5 <- sendMessage alice bobId "hello" 4 <- sendMessage alice bobId "hello"
get alice ##> ("", bobId, SENT 5) get alice ##> ("", bobId, SENT 4)
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
ackMessage bob aliceId 5 ackMessage bob aliceId 4
6 <- sendMessage bob aliceId "hello too" 5 <- sendMessage bob aliceId "hello too"
get bob ##> ("", aliceId, SENT 6) get bob ##> ("", aliceId, SENT 5)
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
ackMessage alice bobId 6 ackMessage alice bobId 5
+136
View File
@@ -0,0 +1,136 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
module AgentTests.PostgresTests (postgresStoreTests) 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
import Data.Text.Encoding (encodeUtf8)
import Data.Time
import Data.Word (Word32)
import Database.PostgreSQL.Simple (ConnectInfo (..), defaultConnectInfo)
import qualified Database.PostgreSQL.Simple as DB
import SMPClient (testKeyHash)
import Simplex.Messaging.Agent.Client ()
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.Postgres
import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations
import qualified Simplex.Messaging.Crypto as C
import System.Random
import Test.Hspec
import UnliftIO.Directory (removeFile)
withStore :: SpecWith PostgresStore -> Spec
withStore = before createStore
createStore :: IO PostgresStore
createStore = do
let dbConnInfo = defaultConnectInfo {connectDatabase = "agent_poc_1"}
createPostgresStore dbConnInfo 1 Migrations.app
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
-- TODO add null port tests
postgresStoreTests :: Spec
postgresStoreTests = do
-- withStore2 $ do
-- describe "stress test" testConcurrentWrites
withStore $ do
-- describe "store setup" $ do
-- testCompiledThreadsafe
-- testForeignKeysEnabled
describe "store methods" $ do
describe "Queue and Connection management" $ do
-- describe "createRcvConn" $ do
-- testCreateRcvConn
-- testCreateRcvConnRandomId
-- testCreateRcvConnDuplicate
fdescribe "createSndConn" $ do
testCreateSndConn
-- testCreateSndConnRandomID
-- testCreateSndConnDuplicate
-- describe "getRcvConn" testGetRcvConn
-- describe "deleteConn" $ do
-- testDeleteRcvConn
-- testDeleteSndConn
-- testDeleteDuplexConn
-- describe "upgradeRcvConnToDuplex" $ do
-- testUpgradeRcvConnToDuplex
-- describe "upgradeSndConnToDuplex" $ do
-- testUpgradeSndConnToDuplex
-- describe "set Queue status" $ do
-- describe "setRcvQueueStatus" $ do
-- testSetRcvQueueStatus
-- describe "setSndQueueStatus" $ do
-- testSetSndQueueStatus
-- testSetQueueStatusDuplex
-- describe "Msg management" $ do
-- describe "create Msg" $ do
-- testCreateRcvMsg
-- testCreateSndMsg
-- testCreateRcvAndSndMsgs
cData1 :: ConnData
cData1 = ConnData {connId = "conn1"}
testPrivateSignKey :: C.APrivateSignKey
testPrivateSignKey = C.APrivateSignKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe"
testPrivDhKey :: C.PrivateKeyX25519
testPrivDhKey = "MC4CAQAwBQYDK2VuBCIEINCzbVFaCiYHoYncxNY8tSIfn0pXcIAhLBfFc0m+gOpk"
testDhSecret :: C.DhSecretX25519
testDhSecret = "01234567890123456789012345678901"
rcvQueue1 :: RcvQueue
rcvQueue1 =
RcvQueue
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
rcvId = "1234",
rcvPrivateKey = testPrivateSignKey,
rcvDhSecret = testDhSecret,
e2ePrivKey = testPrivDhKey,
e2eDhSecret = Nothing,
sndId = Just "2345",
status = New
}
sndQueue1 :: SndQueue
sndQueue1 =
SndQueue
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
sndId = "3456",
sndPrivateKey = testPrivateSignKey,
e2eDhSecret = testDhSecret,
status = New
}
testCreateSndConn :: SpecWith PostgresStore
testCreateSndConn =
it "should create SndConnection and add RcvQueue" $ \store -> 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)
+3 -7
View File
@@ -51,7 +51,7 @@ createStore = do
-- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous -- 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 -- IO operations on multiple similarly named files; error seems to be environment specific
r <- randomIO :: IO Word32 r <- randomIO :: IO Word32
createSQLiteStore (testDB <> show r) 4 Migrations.app True createSQLiteStore (testDB <> show r) 4 Migrations.app
removeStore :: SQLiteStore -> IO () removeStore :: SQLiteStore -> IO ()
removeStore store = do removeStore store = do
@@ -173,9 +173,7 @@ sndQueue1 =
SndQueue SndQueue
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash, { server = SMPServer "smp.simplex.im" "5223" testKeyHash,
sndId = "3456", sndId = "3456",
sndPublicKey = Nothing,
sndPrivateKey = testPrivateSignKey, sndPrivateKey = testPrivateSignKey,
e2ePubKey = Nothing,
e2eDhSecret = testDhSecret, e2eDhSecret = testDhSecret,
status = New status = New
} }
@@ -305,9 +303,7 @@ testUpgradeRcvConnToDuplex =
SndQueue SndQueue
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash, { server = SMPServer "smp.simplex.im" "5223" testKeyHash,
sndId = "2345", sndId = "2345",
sndPublicKey = Nothing,
sndPrivateKey = testPrivateSignKey, sndPrivateKey = testPrivateSignKey,
e2ePubKey = Nothing,
e2eDhSecret = testDhSecret, e2eDhSecret = testDhSecret,
status = New status = New
} }
@@ -397,7 +393,7 @@ mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
sndMsgId = externalSndId, sndMsgId = externalSndId,
broker = (brokerId, ts) broker = (brokerId, ts)
}, },
msgType = AM_A_MSG_, msgType = A_MSG_,
msgBody = hw, msgBody = hw,
internalHash, internalHash,
externalPrevSndHash = "hash_from_sender" externalPrevSndHash = "hash_from_sender"
@@ -426,7 +422,7 @@ mkSndMsgData internalId internalSndId internalHash =
{ internalId, { internalId,
internalSndId, internalSndId,
internalTs = ts, internalTs = ts,
msgType = AM_A_MSG_, msgType = A_MSG_,
msgBody = hw, msgBody = hw,
internalHash, internalHash,
prevMsgHash = internalHash prevMsgHash = internalHash
+28 -19
View File
@@ -10,6 +10,7 @@ import Control.Monad.IO.Unlift
import Crypto.Random import Crypto.Random
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import Database.PostgreSQL.Simple (ConnectInfo (..), defaultConnectInfo)
import Network.Socket (HostName, ServiceName) import Network.Socket (HostName, ServiceName)
import SMPClient import SMPClient
( serverBracket, ( serverBracket,
@@ -20,14 +21,13 @@ import SMPClient
withSmpServerOn, withSmpServerOn,
withSmpServerThreadOn, withSmpServerThreadOn,
) )
import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Env.Postgres
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) import Simplex.Messaging.Agent.Server (runSMPAgentBlocking)
import Simplex.Messaging.Client (SMPClientConfig (..), smpDefaultConfig) import Simplex.Messaging.Client (SMPClientConfig (..), smpDefaultConfig)
import Simplex.Messaging.Transport import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client import Simplex.Messaging.Transport.Client
import Simplex.Messaging.Transport.KeepAlive
import Test.Hspec import Test.Hspec
import UnliftIO.Concurrent import UnliftIO.Concurrent
import UnliftIO.Directory import UnliftIO.Directory
@@ -44,14 +44,23 @@ agentTestPort2 = "5011"
agentTestPort3 :: ServiceName agentTestPort3 :: ServiceName
agentTestPort3 = "5012" agentTestPort3 = "5012"
testDB :: String -- testDB :: String
testDB = "tests/tmp/smp-agent.test.protocol.db" -- testDB = "tests/tmp/smp-agent.test.protocol.db"
testDB2 :: String testDB :: ConnectInfo
testDB2 = "tests/tmp/smp-agent2.test.protocol.db" testDB = defaultConnectInfo {connectDatabase = "agent_poc_1"}
testDB3 :: String -- testDB2 :: String
testDB3 = "tests/tmp/smp-agent3.test.protocol.db" -- testDB2 = "tests/tmp/smp-agent2.test.protocol.db"
testDB2 :: ConnectInfo
testDB2 = defaultConnectInfo {connectDatabase = "agent_poc_2"}
-- testDB3 :: String
-- testDB3 = "tests/tmp/smp-agent3.test.protocol.db"
testDB3 :: ConnectInfo
testDB3 = defaultConnectInfo {connectDatabase = "agent_poc_3"}
smpAgentTest :: forall c. Transport c => TProxy c -> ARawTransmission -> IO ARawTransmission smpAgentTest :: forall c. Transport c => TProxy c -> ARawTransmission -> IO ARawTransmission
smpAgentTest _ cmd = runSmpAgentTest $ \(h :: c) -> tPutRaw h cmd >> tGetRaw h smpAgentTest _ cmd = runSmpAgentTest $ \(h :: c) -> tPutRaw h cmd >> tGetRaw h
@@ -72,10 +81,10 @@ runSmpAgentServerTest test =
smpAgentServerTest :: Transport c => ((ThreadId, ThreadId) -> c -> IO ()) -> Expectation smpAgentServerTest :: Transport c => ((ThreadId, ThreadId) -> c -> IO ()) -> Expectation
smpAgentServerTest test' = runSmpAgentServerTest test' `shouldReturn` () smpAgentServerTest test' = runSmpAgentServerTest test' `shouldReturn` ()
runSmpAgentTestN :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, String)] -> ([c] -> m a) -> m a runSmpAgentTestN :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, ConnectInfo)] -> ([c] -> m a) -> m a
runSmpAgentTestN agents test = withSmpServer t $ run agents [] runSmpAgentTestN agents test = withSmpServer t $ run agents []
where where
run :: [(ServiceName, ServiceName, String)] -> [c] -> m a run :: [(ServiceName, ServiceName, ConnectInfo)] -> [c] -> m a
run [] hs = test hs run [] hs = test hs
run (a@(p, _, _) : as) hs = withSmpAgentOn t a $ testSMPAgentClientOn p $ \h -> run as (h : hs) run (a@(p, _, _) : as) hs = withSmpAgentOn t a $ testSMPAgentClientOn p $ \h -> run as (h : hs)
t = transport @c t = transport @c
@@ -88,7 +97,7 @@ runSmpAgentTestN_1 nClients test = withSmpServer t . withSmpAgent t $ run nClien
run n hs = testSMPAgentClient $ \h -> run (n - 1) (h : hs) run n hs = testSMPAgentClient $ \h -> run (n - 1) (h : hs)
t = transport @c t = transport @c
smpAgentTestN :: Transport c => [(ServiceName, ServiceName, String)] -> ([c] -> IO ()) -> Expectation smpAgentTestN :: Transport c => [(ServiceName, ServiceName, ConnectInfo)] -> ([c] -> IO ()) -> Expectation
smpAgentTestN agents test' = runSmpAgentTestN agents test' `shouldReturn` () smpAgentTestN agents test' = runSmpAgentTestN agents test' `shouldReturn` ()
smpAgentTestN_1 :: Transport c => Int -> ([c] -> IO ()) -> Expectation smpAgentTestN_1 :: Transport c => Int -> ([c] -> IO ()) -> Expectation
@@ -158,9 +167,9 @@ cfg :: AgentConfig
cfg = cfg =
defaultAgentConfig defaultAgentConfig
{ tcpPort = agentTestPort, { tcpPort = agentTestPort,
initialSMPServers = L.fromList ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"], smpServers = L.fromList ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"],
tbqSize = 1, tbqSize = 1,
dbFile = testDB, dbConnInfo = testDB,
smpCfg = smpCfg =
smpDefaultConfig smpDefaultConfig
{ qSize = 1, { qSize = 1,
@@ -173,17 +182,17 @@ cfg =
certificateFile = "tests/fixtures/server.crt" certificateFile = "tests/fixtures/server.crt"
} }
withSmpAgentThreadOn_ :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m () -> (ThreadId -> m a) -> m a withSmpAgentThreadOn_ :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, ConnectInfo) -> m () -> (ThreadId -> m a) -> m a
withSmpAgentThreadOn_ t (port', smpPort', db') afterProcess = withSmpAgentThreadOn_ t (port', smpPort', db') afterProcess =
let cfg' = cfg {tcpPort = port', dbFile = db', initialSMPServers = L.fromList [SMPServer "localhost" smpPort' testKeyHash]} let cfg' = cfg {tcpPort = port', dbConnInfo = db', smpServers = L.fromList [SMPServer "localhost" smpPort' testKeyHash]}
in serverBracket in serverBracket
(\started -> runSMPAgentBlocking t started cfg') (\started -> runSMPAgentBlocking t started cfg')
afterProcess afterProcess
withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, ConnectInfo) -> (ThreadId -> m a) -> m a
withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a $ removeFile db' withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a $ pure () -- $ removeFile db'
withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, ConnectInfo) -> m a -> m a
withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const
withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a
@@ -191,7 +200,7 @@ withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB)
testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m a) -> m a testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m a) -> m a
testSMPAgentClientOn port' client = do testSMPAgentClientOn port' client = do
runTransportClient agentTestHost port' testKeyHash (Just defaultKeepAliveOpts) $ \h -> do runTransportClient agentTestHost port' testKeyHash $ \h -> do
line <- liftIO $ getLn h line <- liftIO $ getLn h
if line == "Welcome to SMP agent v" <> B.pack simplexMQVersion if line == "Welcome to SMP agent v" <> B.pack simplexMQVersion
then client h then client h
+7 -11
View File
@@ -22,7 +22,6 @@ import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Server.StoreLog (openReadStoreLog) import Simplex.Messaging.Server.StoreLog (openReadStoreLog)
import Simplex.Messaging.Transport import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client import Simplex.Messaging.Transport.Client
import Simplex.Messaging.Transport.KeepAlive
import Test.Hspec import Test.Hspec
import UnliftIO.Concurrent import UnliftIO.Concurrent
import qualified UnliftIO.Exception as E import qualified UnliftIO.Exception as E
@@ -46,7 +45,7 @@ testStoreLogFile = "tests/tmp/smp-server-store.log"
testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a
testSMPClient client = testSMPClient client =
runTransportClient testHost testPort testKeyHash (Just defaultKeepAliveOpts) $ \h -> runTransportClient testHost testPort testKeyHash $ \h ->
liftIO (runExceptT $ clientHandshake h testKeyHash) >>= \case liftIO (runExceptT $ clientHandshake h testKeyHash) >>= \case
Right th -> client th Right th -> client th
Left e -> error $ show e Left e -> error $ show e
@@ -61,9 +60,6 @@ cfg =
queueIdBytes = 24, queueIdBytes = 24,
msgIdBytes = 24, msgIdBytes = 24,
storeLog = Nothing, storeLog = Nothing,
allowNewQueues = True,
messageTTL = Just $ 7 * 86400, -- seconds, 7 days
expireMessagesInterval = Just 21600_000000, -- microseconds, 6 hours
caCertificateFile = "tests/fixtures/ca.crt", caCertificateFile = "tests/fixtures/ca.crt",
privateKeyFile = "tests/fixtures/server.key", privateKeyFile = "tests/fixtures/server.key",
certificateFile = "tests/fixtures/server.crt" certificateFile = "tests/fixtures/server.crt"
@@ -72,16 +68,16 @@ cfg =
withSmpServerStoreLogOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a withSmpServerStoreLogOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerStoreLogOn t port' client = do withSmpServerStoreLogOn t port' client = do
s <- liftIO $ openReadStoreLog testStoreLogFile s <- liftIO $ openReadStoreLog testStoreLogFile
withSmpServerConfigOn t cfg {storeLog = Just s} port' client
withSmpServerConfigOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServerConfig -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerConfigOn t cfg' port' =
serverBracket serverBracket
(\started -> runSMPServerBlocking started cfg' {transports = [(port', t)]}) (\started -> runSMPServerBlocking started cfg {transports = [(port', t)], storeLog = Just s})
(pure ()) (pure ())
client
withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerThreadOn t = withSmpServerConfigOn t cfg withSmpServerThreadOn t port' =
serverBracket
(\started -> runSMPServerBlocking started cfg {transports = [(port', t)]})
(pure ())
serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a
serverBracket process afterProcess f = do serverBracket process afterProcess f = do
+2 -68
View File
@@ -9,7 +9,7 @@
module ServerTests where module ServerTests where
import Control.Concurrent (ThreadId, killThread, threadDelay) import Control.Concurrent (ThreadId, killThread)
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Exception (SomeException, try) import Control.Exception (SomeException, try)
import Control.Monad.Except (forM, forM_, runExceptT) import Control.Monad.Except (forM, forM_, runExceptT)
@@ -21,7 +21,6 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.Env.STM (ServerConfig (..))
import Simplex.Messaging.Transport import Simplex.Messaging.Transport
import System.Directory (removeFile) import System.Directory (removeFile)
import System.TimeIt (timeItT) import System.TimeIt (timeItT)
@@ -30,23 +29,18 @@ import Test.HUnit
import Test.Hspec import Test.Hspec
serverTests :: ATransport -> Spec serverTests :: ATransport -> Spec
serverTests t@(ATransport t') = do serverTests t = do
describe "SMP syntax" $ syntaxTests t describe "SMP syntax" $ syntaxTests t
describe "SMP queues" $ do describe "SMP queues" $ do
describe "NEW and KEY commands, SEND messages" $ testCreateSecure t describe "NEW and KEY commands, SEND messages" $ testCreateSecure t
describe "NEW, OFF and DEL commands, SEND messages" $ testCreateDelete t describe "NEW, OFF and DEL commands, SEND messages" $ testCreateDelete t
describe "Stress test" $ stressTest t describe "Stress test" $ stressTest t
describe "allowNewQueues setting" $ testAllowNewQueues t'
describe "SMP messages" $ do describe "SMP messages" $ do
describe "duplex communication over 2 SMP connections" $ testDuplex t describe "duplex communication over 2 SMP connections" $ testDuplex t
describe "switch subscription to another TCP connection" $ testSwitchSub t describe "switch subscription to another TCP connection" $ testSwitchSub t
describe "Store log" $ testWithStoreLog t describe "Store log" $ testWithStoreLog t
describe "Timing of AUTH error" $ testTiming t describe "Timing of AUTH error" $ testTiming t
describe "Message notifications" $ testMessageNotifications t describe "Message notifications" $ testMessageNotifications t
describe "Message expiration" $ do
testMsgExpireOnSend t'
testMsgExpireOnInterval t'
testMsgNOTExpireOnInterval t'
pattern Resp :: CorrId -> QueueId -> BrokerMsg -> SignedTransmission BrokerMsg pattern Resp :: CorrId -> QueueId -> BrokerMsg -> SignedTransmission BrokerMsg
pattern Resp corrId queueId command <- (_, _, (corrId, queueId, Right command)) pattern Resp corrId queueId command <- (_, _, (corrId, queueId, Right command))
@@ -210,16 +204,6 @@ stressTest (ATransport t) =
closeConnection $ connection h2 closeConnection $ connection h2
subscribeQueues h3 subscribeQueues h3
testAllowNewQueues :: forall c. Transport c => TProxy c -> Spec
testAllowNewQueues t =
it "should prohibit creating new queues with allowNewQueues = False" $ do
withSmpServerConfigOn (ATransport t) cfg {allowNewQueues = False} testPort $ \_ ->
testSMPClient @c $ \h -> do
(rPub, rKey) <- C.generateSignatureKeyPair C.SEd448
(dhPub, _ :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" "" (ERR AUTH) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub)
pure ()
testDuplex :: ATransport -> Spec testDuplex :: ATransport -> Spec
testDuplex (ATransport t) = testDuplex (ATransport t) =
it "should create 2 simplex connections and exchange messages" $ it "should create 2 simplex connections and exchange messages" $
@@ -482,56 +466,6 @@ testMessageNotifications (ATransport t) =
Nothing -> return () Nothing -> return ()
Just _ -> error "nothing else should be delivered to the 1st notifier's TCP connection" Just _ -> error "nothing else should be delivered to the 1st notifier's TCP connection"
testMsgExpireOnSend :: forall c. Transport c => TProxy c -> Spec
testMsgExpireOnSend t =
it "should expire messages that are not received before messageTTL on SEND" $ do
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
withSmpServerConfigOn (ATransport t) cfg {messageTTL = Just 1} testPort $ \_ ->
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)")
threadDelay 2500000
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)
(dec mId msg, Right "hello (should NOT expire)") #== "delivered"
1000 `timeout` tGet @BrokerMsg rh >>= \case
Nothing -> return ()
Just _ -> error "nothing else should be delivered"
testMsgExpireOnInterval :: forall c. Transport c => TProxy c -> Spec
testMsgExpireOnInterval t =
it "should expire messages that are not received before messageTTL after expiry interval" $ do
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
withSmpServerConfigOn (ATransport t) cfg {messageTTL = Just 1, expireMessagesInterval = Just 1000000} 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)")
threadDelay 2500000
testSMPClient @c $ \rh -> do
Resp "2" _ OK <- signSendRecv rh rKey ("2", rId, SUB)
1000 `timeout` tGet @BrokerMsg rh >>= \case
Nothing -> return ()
Just _ -> error "nothing should be delivered"
testMsgNOTExpireOnInterval :: forall c. Transport c => TProxy c -> Spec
testMsgNOTExpireOnInterval t =
it "should NOT expire messages that are not received before messageTTL if expiry interval is not set" $ do
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
withSmpServerConfigOn (ATransport t) cfg {messageTTL = Just 1, expireMessagesInterval = Nothing} testPort $ \_ ->
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)")
threadDelay 2500000
testSMPClient @c $ \rh -> do
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 ()
Just _ -> error "nothing else should be delivered"
samplePubKey :: C.APublicVerifyKey samplePubKey :: C.APublicVerifyKey
samplePubKey = C.APublicVerifyKey C.SEd25519 "MCowBQYDK2VwAyEAfAOflyvbJv1fszgzkQ6buiZJVgSpQWsucXq7U6zjMgY=" samplePubKey = C.APublicVerifyKey C.SEd25519 "MCowBQYDK2VwAyEAfAOflyvbJv1fszgzkQ6buiZJVgSpQWsucXq7U6zjMgY="