mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-01 18:08:36 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c298728e2 | ||
|
|
cca8ac5a58 | ||
|
|
b1d2d45947 | ||
|
|
c9c6d2b2d3 | ||
|
|
85c09d1703 | ||
|
|
08b43b42a0 | ||
|
|
4980db932d | ||
|
|
b2fbab5b0f |
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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;
|
|
||||||
|]
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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) ()
|
||||||
|
|||||||
@@ -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,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 ()
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 #-}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 #-}
|
||||||
|
|||||||
@@ -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
@@ -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" $
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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="
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user