mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-12 02:22:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b0950e78e | ||
|
|
aff2d73e45 | ||
|
|
377b166d8e | ||
|
|
1c7d7e5083 | ||
|
|
772b38d154 | ||
|
|
b517d793d8 | ||
|
|
76e81e1cf0 | ||
|
|
4ec548f8b8 | ||
|
|
885dda21ec | ||
|
|
8aabb6f0e6 | ||
|
|
d4378461ab | ||
|
|
28ddef2d9c | ||
|
|
7df39e9db3 | ||
|
|
aac23c6481 | ||
|
|
af408d50bf | ||
|
|
93956b65c2 | ||
|
|
b7902ee4c8 | ||
|
|
633b3a4bda | ||
|
|
829c198e5f | ||
|
|
6be4839703 | ||
|
|
6ceeb2c9db | ||
|
|
bf6e652e41 | ||
|
|
9a8570c59c | ||
|
|
729cf10ad8 | ||
|
|
afc09a6ec4 | ||
|
|
816703527a | ||
|
|
5fec6c1755 | ||
|
|
cddff78719 | ||
|
|
65ad2eb6a8 | ||
|
|
3187bc8140 | ||
|
|
40ad6db51a | ||
|
|
5e3bc7ee6c | ||
|
|
9104ff0c4a | ||
|
|
65db5cfa18 | ||
|
|
5c1e9682c8 | ||
|
|
36dad0ba86 | ||
|
|
417066c462 | ||
|
|
326050651a | ||
|
|
0028939155 | ||
|
|
2eb5ce24ec | ||
|
|
c4475499ba | ||
|
|
19520787a6 | ||
|
|
b364f168c8 | ||
|
|
a113067647 | ||
|
|
f6888cc9a2 | ||
|
|
07410990a0 | ||
|
|
dbe1c4a781 | ||
|
|
45514c0a65 | ||
|
|
6c7e013007 | ||
|
|
a594972df0 | ||
|
|
e78b7a66d7 | ||
|
|
a2016241df | ||
|
|
5c44ad0a62 | ||
|
|
d0b047d197 | ||
|
|
b8241af7e9 | ||
|
|
44c5a3142e | ||
|
|
ad73298936 | ||
|
|
59ef46314d | ||
|
|
0fbf406800 | ||
|
|
d8965d4a23 | ||
|
|
4353dcb3a7 | ||
|
|
16eb560dbc | ||
|
|
bd753dbe47 | ||
|
|
08171e987d | ||
|
|
da72efc20a | ||
|
|
7cad237092 | ||
|
|
9b0ed1d62d | ||
|
|
b9943c359d | ||
|
|
11c8bee836 | ||
|
|
93e782b874 | ||
|
|
cc9a67f38a | ||
|
|
660e35d1d1 | ||
|
|
a3990ea170 | ||
|
|
d96aeb727f | ||
|
|
927ff230da | ||
|
|
7570ef9e22 | ||
|
|
3af34dea8b | ||
|
|
135aac77ab | ||
|
|
8134bb0eb9 | ||
|
|
a0c52033f7 | ||
|
|
dac35d8ea8 | ||
|
|
b4ebe85ce3 | ||
|
|
0f60c53a66 | ||
|
|
dca1bee926 | ||
|
|
1f38beb23d | ||
|
|
51aab978bd | ||
|
|
3a161e93ca | ||
|
|
1251caa4e8 | ||
|
|
033af51d6d | ||
|
|
f3840928e0 | ||
|
|
8d05ace601 | ||
|
|
1cf1c6a4b1 | ||
|
|
c25a6d6896 | ||
|
|
435ab21e71 | ||
|
|
469f84bb74 | ||
|
|
0fe41dbf16 | ||
|
|
ce362da311 | ||
|
|
7e9cfcce8c | ||
|
|
1f61267308 | ||
|
|
3192092349 | ||
|
|
4b5349fb94 | ||
|
|
17b429afe7 | ||
|
|
e09d3bae99 | ||
|
|
d719b741dc | ||
|
|
19dc7b3389 | ||
|
|
f50da16d0a | ||
|
|
d82c286a54 | ||
|
|
c30a4cd1ff | ||
|
|
70fe7616f2 | ||
|
|
65dd693c83 | ||
|
|
d47b57ac87 | ||
|
|
c14b0251d5 | ||
|
|
8ab79b066d | ||
|
|
10fb667ff3 | ||
|
|
c1f15c9d93 | ||
|
|
3efb15ecb3 | ||
|
|
5f59fcc969 | ||
|
|
72a1082822 | ||
|
|
2e6ba85308 | ||
|
|
b02bd42f84 | ||
|
|
02a15f9f25 | ||
|
|
1c5325babf | ||
|
|
241e3a29a8 | ||
|
|
7522d8b24f | ||
|
|
df16ce1dfc | ||
|
|
26007818ff | ||
|
|
253f4e39c9 | ||
|
|
75570350a8 | ||
|
|
f91e0c7387 | ||
|
|
123fe0e3e2 | ||
|
|
7f22dcd496 | ||
|
|
6f8b3787b0 | ||
|
|
f5aaab3861 | ||
|
|
b9cdc6a09d | ||
|
|
ef028c135d | ||
|
|
394803b3cf | ||
|
|
ea6496ca24 | ||
|
|
d1c5e9df7b | ||
|
|
3dbdb948ae | ||
|
|
a1c4ce6448 | ||
|
|
0d0a12f778 | ||
|
|
75f58f8ba4 | ||
|
|
af5bd101cb | ||
|
|
19238c6cc4 | ||
|
|
6dff023965 | ||
|
|
e692cb0119 | ||
|
|
51cc602bdb | ||
|
|
5a6b1c67a9 | ||
|
|
94d0be5d27 | ||
|
|
61092e8371 | ||
|
|
1c32fccb39 | ||
|
|
6e6ad0fb9b | ||
|
|
a555c4e510 | ||
|
|
4a19602be6 | ||
|
|
a28462d496 | ||
|
|
799d179f6f | ||
|
|
5d2e77f41d | ||
|
|
f8e25189db | ||
|
|
2e761b20f6 | ||
|
|
9a1ffb349a | ||
|
|
15cf2e9fff | ||
|
|
650971fa02 | ||
|
|
3d4f43a051 | ||
|
|
6526e421e8 | ||
|
|
48967167c4 | ||
|
|
d260a464d6 | ||
|
|
24bca7650e | ||
|
|
42ab7fb6ac | ||
|
|
80b4ff365d | ||
|
|
f3cd3eac58 | ||
|
|
3c1834f93f | ||
|
|
44b58a5bf0 | ||
|
|
cdf8695bef | ||
|
|
d6e1aeacdf | ||
|
|
92d170f178 | ||
|
|
fdf35ba214 | ||
|
|
9d15035fb3 | ||
|
|
dabef7f0d1 | ||
|
|
cf38b4dddf | ||
|
|
c47f583574 | ||
|
|
dea5c6478a | ||
|
|
205c6bf117 | ||
|
|
87f2c20ee4 | ||
|
|
a61d97aee0 | ||
|
|
43baacc5be | ||
|
|
d95936a770 | ||
|
|
f61ad27fcd | ||
|
|
7f1d3da202 | ||
|
|
e7581a91a8 | ||
|
|
4b8f6417f8 | ||
|
|
02cb6068a2 | ||
|
|
9d8df33cf7 | ||
|
|
a6bd03bf99 | ||
|
|
e418498334 | ||
|
|
18ab9650b4 | ||
|
|
7bcd5ebcef | ||
|
|
a3b53f68c5 | ||
|
|
501e8a140a | ||
|
|
027b91098d | ||
|
|
f7e0b26297 | ||
|
|
c195c79367 | ||
|
|
2a87a1b5c9 | ||
|
|
d4c4c467f4 | ||
|
|
14d7619bbf | ||
|
|
c5a8bf6f40 | ||
|
|
2362a22f7a | ||
|
|
b234c0afee | ||
|
|
2ed24e683e | ||
|
|
6249bf749e | ||
|
|
990ff1d7ed | ||
|
|
c35b838e06 | ||
|
|
77a532ff13 | ||
|
|
3eb33363a0 | ||
|
|
c53cbac8d2 | ||
|
|
f20593c422 | ||
|
|
456eb0634f | ||
|
|
e2a4f09684 | ||
|
|
fe66b35192 | ||
|
|
42aec97cd3 | ||
|
|
10b109e384 | ||
|
|
e3ea6aa245 | ||
|
|
968f3b605a | ||
|
|
716a8a2067 | ||
|
|
3cb2421373 | ||
|
|
28a3db5edc | ||
|
|
efe799f460 | ||
|
|
f8aa11f419 | ||
|
|
adc8af16b3 | ||
|
|
fc2e51053a | ||
|
|
dc223f564d | ||
|
|
ea77a4653f | ||
|
|
b80fd37a0f | ||
|
|
9d9afb1150 | ||
|
|
12e9fbf75a | ||
|
|
43c2c9d02d | ||
|
|
bea5f0525a | ||
|
|
50074eb33b | ||
|
|
dfa80d6756 | ||
|
|
1b81010e4a | ||
|
|
91470bc65d | ||
|
|
b9f4c50771 | ||
|
|
d1022dc7fc | ||
|
|
b469a90116 | ||
|
|
90d6857128 |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"template": "${{UNCATEGORIZED}}",
|
||||
"pr_template": "- ${{TITLE}}\n"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build changelog
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
with:
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
ignorePreReleases: true
|
||||
commitMode: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{ steps.build_changelog.outputs.changelog }}
|
||||
files: |
|
||||
LICENSE
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build:
|
||||
name: build-${{ matrix.os }}
|
||||
if: always()
|
||||
needs: prepare-release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
asset_name: smp-server-ubuntu-20_04-x86-64
|
||||
- os: ubuntu-18.04
|
||||
asset_name: smp-server-ubuntu-18_04-x86-64
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Stack
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: '8.8.4'
|
||||
enable-stack: true
|
||||
stack-version: 'latest'
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.stack
|
||||
key: ${{ matrix.os }}-${{ hashFiles('stack.yaml') }}
|
||||
|
||||
- name: Build & test
|
||||
id: build_test
|
||||
run: |
|
||||
stack build --test
|
||||
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"
|
||||
|
||||
- name: Upload binaries to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.build_test.outputs.LOCAL_INSTALL_ROOT }}/bin/smp-server
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
@@ -1,2 +1,4 @@
|
||||
*.lock
|
||||
*.cabal
|
||||
*.db
|
||||
*.session.sql
|
||||
|
||||
@@ -1,93 +1,69 @@
|
||||
# simplex-messaging
|
||||
# SimpleXMQ
|
||||
|
||||
## SMP server demo
|
||||
[](https://github.com/simplex-chat/simplexmq/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplexmq/releases)
|
||||
|
||||
This is a demo implementation of SMP ([simplex messaging protocol](https://github.com/simplex-chat/protocol/blob/master/simplex-messaging.md)) server.
|
||||
## Message broker for unidirectional (simplex) queues
|
||||
|
||||
It has a very limited utility (if any) for real applications, as it lacks the following protocol features:
|
||||
SimpleXMQ is a message broker for managing message queues and sending messages over public network. It consists of SMP server, SMP client library and SMP agent that implement [SMP protocol](./protocol/simplex-messaging.md) for client-server communication and [SMP agent protocol](./protocol/agent-protocol.md) to manage duplex connections via simplex queues on multiple SMP servers.
|
||||
|
||||
- cryptographic signature verification, instead it simply compares provided "signature" with stored "public key", effectively treating them as plain text passwords.
|
||||
- there is no transport encryption
|
||||
SMP protocol is inspired by [Redis serialization protocol](https://redis.io/topics/protocol), but it is much simpler - it currently has only 8 client commands and 6 server responses.
|
||||
|
||||
Because of these limitations, it is easy to experiment with the protocol logic via telnet.
|
||||
SimpleXMQ is implemented in Haskell - it benefits from robust software transactional memory (STM) and concurrency primitives that Haskell provides.
|
||||
|
||||
You can either run it locally or try with the deployed demo server:
|
||||
## SimpleXMQ roadmap
|
||||
|
||||
```bash
|
||||
telnet smp.simplex.im 5223
|
||||
```
|
||||
- Streams - high performance message queues. See [Streams RFC](./rfcs/2021-02-28-streams.md) for details.
|
||||
- "Small" connection groups, when each message will be sent by the SMP agent to multiple connections with a single client command. See [Groups RFC](./rfcs/2021-03-18-groups.md) for details.
|
||||
- SMP agents cluster to share connections and message management by multiple agents (for example, it would enable multi-device use for [simplex-chat](https://github.com/simplex-chat/simplex-chat)).
|
||||
- SMP queue redundancy and rotation in SMP agent duplex connections.
|
||||
- "Large" groups design and implementation.
|
||||
|
||||
## Run locally
|
||||
## Components
|
||||
|
||||
[Install stack](https://docs.haskellstack.org/en/stable/install_and_upgrade/) and `stack run`.
|
||||
### SMP server
|
||||
|
||||
## Usage example
|
||||
[SMP server](./apps/smp-server/Main.hs) can be run on any Linux distribution without any dependencies. It 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.
|
||||
|
||||
Lines you should send are prefixed with `>` character, you should not type them.
|
||||
To enable the queue logging, uncomment `enable: on` option in `smp-server.ini` configuration file that is created the first time the server is started.
|
||||
|
||||
Comments are prefixed with `--`, they are not part of transmissions.
|
||||
On the first start the server generates an RSA key pair for encrypted transport handshake and outputs hash of the public key every time it runs - this hash should be used as part of the server address: `<hostname>:5223#<key hash>`.
|
||||
|
||||
`>` on its own means you need to press `return` - telnet should be configured to send it as CRLF.
|
||||
SMP server implements [SMP protocol](./protocol/simplex-messaging.md).
|
||||
|
||||
1. Create simplex message queue:
|
||||
### SMP client library
|
||||
|
||||
```telnet
|
||||
>
|
||||
>
|
||||
> NEW 1234 -- 1234 is recipient's key
|
||||
[SMP client](./src/Simplex/Messaging/Client.hs) is a Haskell library to connect to SMP servers that allows to:
|
||||
- execute commands with a functional API.
|
||||
- receive messages and other notifications via STM queue.
|
||||
- automatically send keep-alive commands.
|
||||
|
||||
### SMP agent
|
||||
|
||||
IDS QuCLU4YxgS7wcPFA YB4CCATREHkaQcEh -- recipient and sender IDs for the queue
|
||||
```
|
||||
[SMP agent library](./src/Simplex/Messaging/Agent.hs) can be used to run SMP agent as part of another application and to communicate with the agent via STM queues, without serializing and parsing commands and responses.
|
||||
|
||||
2. Sender can send their "key" to the queue:
|
||||
Haskell type [ACommand](./src/Simplex/Messaging/Agent/Transmission.hs) represents SMP agent protocol to communicate via STM queues.
|
||||
|
||||
```telnet
|
||||
> -- no signature (just press enter)
|
||||
> YB4CCATREHkaQcEh -- sender ID for the queue
|
||||
> SEND :key abcd
|
||||
See [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI for the example of integrating SMP agent into another application.
|
||||
|
||||
YB4CCATREHkaQcEh
|
||||
OK
|
||||
```
|
||||
[SMP agent executable](./apps/smp-agent/Main.hs) can be used to run a standalone SMP agent process that implements plaintext [SMP agent protocol](./protocol/agent-protocol.md) via TCP port 5224, so it can be used via telnet. It can be deployed in private networks to share access to the connections between multiple applications and services.
|
||||
|
||||
3. Secure queue with sender's "key"
|
||||
## Using SMP server and SMP agent
|
||||
|
||||
```telnet
|
||||
> 1234 -- recipient's "signature" - same as "key" in the demo
|
||||
> QuCLU4YxgS7wcPFA -- recipient ID
|
||||
> KEY abcd -- "key" provided by sender
|
||||
You can either run SMP server locally or try local SMP agent with the deployed demo server:
|
||||
|
||||
QuCLU4YxgS7wcPFA
|
||||
OK
|
||||
```
|
||||
`smp1.simplex.im:5223#pLdiGvm0jD1CMblnov6Edd/391OrYsShw+RgdfR0ChA=`
|
||||
|
||||
4. Sender can now send messages to the queue
|
||||
It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.
|
||||
|
||||
```telnet
|
||||
> abcd -- sender's "signature" - same as "key" in the demo
|
||||
> YB4CCATREHkaQcEh -- sender ID
|
||||
> SEND :hello
|
||||
## SMP server design
|
||||
|
||||
YB4CCATREHkaQcEh
|
||||
OK
|
||||
```
|
||||

|
||||
|
||||
5. Recipient recieves the message and acknowledges it to receive further messages
|
||||
## SMP agent design
|
||||
|
||||
```telnet
|
||||

|
||||
|
||||
QuCLU4YxgS7wcPFA
|
||||
MSG ECA3w3ID 2020-10-18T20:19:36.874Z 5
|
||||
hello
|
||||
> 1234
|
||||
> QuCLU4YxgS7wcPFA
|
||||
> ACK
|
||||
## License
|
||||
|
||||
QuCLU4YxgS7wcPFA
|
||||
OK
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||

|
||||
[AGPL v3](./LICENSE)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Agent (runSMPAgent)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Client (smpDefaultConfig)
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
{ tcpPort = "5224",
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = "smp-agent.db",
|
||||
smpCfg = smpDefaultConfig
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
|
||||
setLogLevel LogInfo -- LogError
|
||||
withGlobalLogging logCfg $ runSMPAgent cfg
|
||||
+156
-7
@@ -1,18 +1,167 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Env.STM
|
||||
import Server (runSMPServer)
|
||||
import Control.Monad (unless, when)
|
||||
import qualified Crypto.Store.PKCS8 as S
|
||||
import Data.ByteString.Base64 (encode)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (toLower)
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import qualified Data.Text as T
|
||||
import Data.X509 (PrivKey (PrivKeyRSA))
|
||||
import Options.Applicative
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Simplex.Messaging.Server.StoreLog (StoreLog, openReadStoreLog)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (combine)
|
||||
import System.IO (IOMode (..), hFlush, stdout)
|
||||
|
||||
cfg :: Config
|
||||
cfg :: ServerConfig
|
||||
cfg =
|
||||
Config
|
||||
ServerConfig
|
||||
{ tcpPort = "5223",
|
||||
tbqSize = 16,
|
||||
queueIdBytes = 12,
|
||||
msgIdBytes = 6
|
||||
msgIdBytes = 6,
|
||||
storeLog = Nothing,
|
||||
-- key is loaded from the file server_key in /etc/opt/simplex directory
|
||||
serverPrivateKey = undefined
|
||||
}
|
||||
|
||||
newKeySize :: Int
|
||||
newKeySize = 2048 `div` 8
|
||||
|
||||
cfgDir :: FilePath
|
||||
cfgDir = "/etc/opt/simplex"
|
||||
|
||||
logDir :: FilePath
|
||||
logDir = "/var/opt/simplex"
|
||||
|
||||
defaultStoreLogFile :: FilePath
|
||||
defaultStoreLogFile = combine logDir "smp-server-store.log"
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "Listening on port " ++ tcpPort cfg
|
||||
runSMPServer cfg
|
||||
opts <- getServerOpts
|
||||
putStrLn "SMP Server (-h for help)"
|
||||
ini <- readCreateIni opts
|
||||
storeLog <- openStoreLog ini
|
||||
pk <- readCreateKey
|
||||
B.putStrLn $ "transport key hash: " <> serverKeyHash pk
|
||||
putStrLn $ "listening on port " <> tcpPort cfg
|
||||
runSMPServer cfg {serverPrivateKey = pk, storeLog}
|
||||
|
||||
data IniOpts = IniOpts
|
||||
{ enableStoreLog :: Bool,
|
||||
storeLogFile :: FilePath
|
||||
}
|
||||
|
||||
readCreateIni :: ServerOpts -> IO IniOpts
|
||||
readCreateIni ServerOpts {configFile} = do
|
||||
createDirectoryIfMissing True cfgDir
|
||||
doesFileExist configFile >>= (`unless` createIni)
|
||||
readIni
|
||||
where
|
||||
readIni :: IO IniOpts
|
||||
readIni = do
|
||||
ini <- either exitError pure =<< readIniFile configFile
|
||||
let enableStoreLog = (== Right "on") $ lookupValue "STORE_LOG" "enable" ini
|
||||
storeLogFile = either (const defaultStoreLogFile) T.unpack $ lookupValue "STORE_LOG" "file" ini
|
||||
pure IniOpts {enableStoreLog, storeLogFile}
|
||||
exitError e = do
|
||||
putStrLn $ "error reading config file " <> configFile <> ": " <> e
|
||||
exitFailure
|
||||
createIni :: IO ()
|
||||
createIni = do
|
||||
confirm $ "Save default ini file to " <> configFile
|
||||
writeFile
|
||||
configFile
|
||||
"[STORE_LOG]\n\
|
||||
\# The server uses STM memory to store SMP queues and messages,\n\
|
||||
\# that will be lost on restart (e.g., as with redis).\n\
|
||||
\# This option enables saving SMP queues to append only log,\n\
|
||||
\# and restoring them when the server is started.\n\
|
||||
\# Log is compacted on start (deleted queues are removed).\n\
|
||||
\# The messages in the queues are not logged.\n\
|
||||
\\n\
|
||||
\# enable: on\n\
|
||||
\# file: /var/opt/simplex/smp-server-store.log\n"
|
||||
|
||||
readCreateKey :: IO C.FullPrivateKey
|
||||
readCreateKey = do
|
||||
createDirectoryIfMissing True cfgDir
|
||||
let path = combine cfgDir "server_key"
|
||||
hasKey <- doesFileExist path
|
||||
(if hasKey then readKey else createKey) path
|
||||
where
|
||||
createKey :: FilePath -> IO C.FullPrivateKey
|
||||
createKey path = do
|
||||
confirm "Generate new server key pair"
|
||||
(_, pk) <- C.generateKeyPair newKeySize
|
||||
S.writeKeyFile S.TraditionalFormat path [PrivKeyRSA $ C.rsaPrivateKey pk]
|
||||
pure pk
|
||||
readKey :: FilePath -> IO C.FullPrivateKey
|
||||
readKey path = do
|
||||
S.readKeyFile path >>= \case
|
||||
[S.Unprotected (PrivKeyRSA pk)] -> pure $ C.FullPrivateKey pk
|
||||
[_] -> errorExit "not RSA key"
|
||||
[] -> errorExit "invalid key file format"
|
||||
_ -> errorExit "more than one key"
|
||||
where
|
||||
errorExit :: String -> IO b
|
||||
errorExit e = putStrLn (e <> ": " <> path) >> exitFailure
|
||||
|
||||
confirm :: String -> IO ()
|
||||
confirm msg = do
|
||||
putStr $ msg <> " (y/N): "
|
||||
hFlush stdout
|
||||
ok <- getLine
|
||||
when (map toLower ok /= "y") exitFailure
|
||||
|
||||
serverKeyHash :: C.FullPrivateKey -> B.ByteString
|
||||
serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey
|
||||
|
||||
openStoreLog :: IniOpts -> IO (Maybe (StoreLog 'ReadMode))
|
||||
openStoreLog IniOpts {enableStoreLog, storeLogFile = f}
|
||||
| enableStoreLog = do
|
||||
createDirectoryIfMissing True logDir
|
||||
putStrLn ("store log: " <> f)
|
||||
Just <$> openReadStoreLog f
|
||||
| otherwise = putStrLn "store log disabled" $> Nothing
|
||||
|
||||
newtype ServerOpts = ServerOpts
|
||||
{ configFile :: FilePath
|
||||
}
|
||||
|
||||
serverOpts :: Parser ServerOpts
|
||||
serverOpts =
|
||||
ServerOpts
|
||||
<$> strOption
|
||||
( long "config"
|
||||
<> short 'c'
|
||||
<> metavar "INI_FILE"
|
||||
<> help ("config file (" <> defaultIniFile <> ")")
|
||||
<> value defaultIniFile
|
||||
)
|
||||
where
|
||||
defaultIniFile = combine cfgDir "smp-server.ini"
|
||||
|
||||
getServerOpts :: IO ServerOpts
|
||||
getServerOpts = execParser opts
|
||||
where
|
||||
opts =
|
||||
info
|
||||
(serverOpts <**> helper)
|
||||
( fullDesc
|
||||
<> header "Simplex Messaging Protocol (SMP) Server"
|
||||
<> progDesc "Start server with INI_FILE (created on first run)"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# SMP agent commands (Duplex protocol commands)
|
||||
|
||||
This document describes Duplex commands flow through SMP agent components (see *design/agent.gv*), as well as some interactions between [recipient and sender](#Recipient-and-sender-terminology) clients, clients' SMP agents and SMP servers, and sequencing of Duplex and SMP commands.
|
||||
|
||||
Legend:
|
||||
|
||||
- > Command flow through SMP agent in blockquotes
|
||||
|
||||
- **Q - Design and other questions in bold**
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Recipient and sender terminology](#Recipient-and-sender-terminology)
|
||||
- [Agent design comparison](#agent-design-comparison)
|
||||
- [Communication between user client and client-side SMP agent](#Communication-between-user-client-and-client-side-SMP-agent)
|
||||
- [User client commands](#User-client-commands)
|
||||
- [`create`](#create)
|
||||
- [`join`](#join)
|
||||
- [`accept`](#accept)
|
||||
- [`subscribe`](#subscribe)
|
||||
- [`getStatus`](#getStatus)
|
||||
- [`send`](#send)
|
||||
- [`acknowledge`](#acknowledge)
|
||||
- [`suspend`](#suspend)
|
||||
- [`delete`](#delete)
|
||||
- [Agent messages to user client](#Agent-messages-to-user-client)
|
||||
- [`connection`](#connection)
|
||||
- [`connectionInvitation`](#connectionInvitation)
|
||||
- [`confirmation`](#confirmation)
|
||||
- [`message`](#message)
|
||||
- [`unsubscribed`](#unsubscribed)
|
||||
- [`queueStatus`](#queueStatus)
|
||||
- [`ok`](#ok)
|
||||
- [`error`](#error)
|
||||
- [Communication between SMP agents](#Communication-between-SMP-agents)
|
||||
- [`helloMsg`](#helloMsg)
|
||||
- [`replyQueueMsg`](#replyQueueMsg)
|
||||
- [`deleteQueueMsg`](#deleteQueueMsg)
|
||||
- [`clientMsg`](#clientMsg)
|
||||
- [`acknowledgeMsg`](#acknowledgeMsg)
|
||||
- [General questions](#General-questions)
|
||||
|
||||
## Recipient and sender terminology
|
||||
|
||||
Here and further recipient is referred to the side that initiates the connection, i.e. creates the initial SMP queue and sends an out-of-band invitation to the sender. Sender is referred to the side that joins this SMP queue through the out-of-band invitation and then creates another SMP queue in the opposite direction for the recipient. This is to distinguish the sides for commands that imply specific direction, conceptually in Duplex connection once connection is established both sides act both as a recipient and a sender through different SMP queues.
|
||||
|
||||
## Agent design comparison
|
||||
|
||||
| Option | Pros | Cons |
|
||||
| ------ | ---- | ---- |
|
||||
| agent.gv | one TCP connection to a specific SMP server host is re-used by multiple users. | responses from the same server to multiple users are mixed and have to be correlated not only with commands but with the users as well, this correlation is not present in the design yet |
|
||||
| agent2.gv | easier correlation of commands with responses, clear separation of internal user agent API (TBQueues) from TCP api (although this can be done with the first design as well) | if there are multiple users, there may be multiple TCP connections to the same SMP host |
|
||||
|
||||
## Communication between user client and client-side SMP agent
|
||||
|
||||
### User client commands
|
||||
|
||||
#### `create`
|
||||
|
||||
🚧 **To be fixed according to agent2.gv**
|
||||
|
||||
Requests recipient SMP server to create a new SMP queue.
|
||||
|
||||
Is made by the [recipient](#Recipient-and-sender-terminology).
|
||||
|
||||
> recipient user client -> user agent TCP socket (main socket) -> runUser thread -> create user connection group (socket and threads) -> user connection group "user" thread -> agent server commands queue -> runServer thread -> SMP `create` command sent to server connection group "send" queue -> server connection group "send" thread -> SMP server connection TCP socket -> recipient's SMP server
|
||||
|
||||
- SMP Server then responds with SMP `queueIds` command to SMP server connection TCP socket
|
||||
- Recipient agent gets automatically subscribed to receive the messages from the queue
|
||||
- Agent replies to user client with Duplex [`connectionInvitation`](#connectionInvitation) command
|
||||
- User sends connection invitation out-of-band to sender
|
||||
|
||||
- **Q - Shouldn't user connection group be created after server responds, or even after sender joins and invites back?**
|
||||
|
||||
A - Agent has to correlate commands with responses anyway (replying on the fact that responses arrive in the same order that the commands were sent, occasionally interleaved with messages), so all the information needed to create the connection will be available in the command buffer anyway. It is probably better to create connection in the agent after the queue was successfully created in the server, and send an error to the user if SMP server responded with error.
|
||||
|
||||
- **Q - How does flow go from runUser thread to user connection group "user" thread? Does runUser pass the the initial `create` command further to "user" thread through socket?**
|
||||
|
||||
A - runUser does not receive any TCP traffic other than initial TCP handshake, when it does it forks all user group threads. It does not participate in commands/messages flow. Commands go from "user" to "runServer" (via TBQueue), responses,messages and acknowledgements (see protocol changes) from "message subscriber" to "send" (via TBQueue)
|
||||
|
||||
- **Q - Does sender agent persist partial info about connection record at this point, or only when receives an opposing queue from the sender?**
|
||||
|
||||
A - Yes, it should create connection with one queue (status "PENDING"), the status of the second queue will be "NONE" - see sequence diagram. In types it is `Maybe ConnState` with "NONE" corresponding to `Nothing`, as we don't need to have the second queue record. Also, protocol allows not to have reply queue, so the connection can remain unidirectional.
|
||||
|
||||
- **Q - Shouldn't agent at this point answer with Duplex [`connection`](#connection) command? SMP agent commands have conflicting comments in the Duplex protocol. What is that command used for at all at this point?**
|
||||
|
||||
A - There is no need to split creating connection and creating the invitation - it was initially two commands, and now it's merged into one - it creates the queue on SMP server, connection record in the agent and the invitation string to be sent out-of-band to the other party - need to fix any contradictions.
|
||||
|
||||
#### `join`
|
||||
|
||||
🚧 **To be fixed according to agent2.gv**
|
||||
|
||||
Replies via out-of-band invitation with sender's key and profile.
|
||||
|
||||
Is made by the [sender](#Recipient-and-sender-terminology).
|
||||
|
||||
> sender user client -> user agent TCP socket (main socket) -> runUser thread -> create user connection group (socket and threads) -> user connection group "user" thread -> agent server commands queue -> runServer thread -> SMP `send` command sent to server connection group "send" queue, it contains sender's key and profile wrapped in Duplex [`clientMsg`](#clientMsg) command -> server connection group "send" thread -> SMP server connection TCP socket -> recipient's SMP server
|
||||
|
||||
- SMP server then delivers sender's message containing key and profile to recipient SMP agent with SMP `message` command
|
||||
- Recipient agent secures previously created queue with Duplex [`accept`](#accept) command
|
||||
|
||||
#### `accept`
|
||||
|
||||
🚧 **To be fixed according to agent2.gv**
|
||||
|
||||
Secures SMP queue with sender's key.
|
||||
|
||||
Is made by the [recipient](#Recipient-and-sender-terminology).
|
||||
|
||||
> recipient user client -> user connection TCP socket -> user connection group "user" thread -> agent server commands queue -> runServer thread -> SMP `secure` command sent to server connection group "send" queue -> server connection group "send" thread -> SMP server connection TCP socket -> recipient's SMP server
|
||||
|
||||
- Sender's agent at this point repeatedly sends Duplex [`helloMsg`](#helloMsg) command to the recipient SMP server, which should be successful once sender queue is secured by the recipient agent with key provided by the sender in the Duplex [`join`](#join) command
|
||||
- **Q - Does this command go through user agent TCP socket (main socket)? This relates to the question whether user connection groups for the recipient agent should be created during Duplex [`create`](#create) command**
|
||||
|
||||
A - User connection group is created during TCP handshake.
|
||||
|
||||
#### `subscribe`
|
||||
|
||||
Subscribes/unsubscribes user to/from SMP queue.
|
||||
|
||||
> agent client -> user connection TCP socket -> "user receive" thread -> user receive TBQueue -> "process commands" thread -> \*
|
||||
>
|
||||
> \* -> commands TBQueue -> client thread; client thread is blocked until receives answer from SMP server -> \#
|
||||
>
|
||||
> \* -> srv snd TBQueue -> "server send" thread -> `subscribe` SMP command is sent through SMP client connection TCP socket -> SMP server responds with first available message or if there is none with SMP `ok` command back to SMP client connection TCP socket -> "server receive" thread -> srv rcv TBQueue -> client thread; client thread becomes unblocked -> \#
|
||||
>
|
||||
> \# -> user SMP TBQueue -> "process responses" thread -> queue is persisted as Disabled -> Duplex [`ok`](#ok) command sent to user send TBQueue -> "user send" thread -> user connection TCP socket -> agent client
|
||||
|
||||
- **Q - Which SMP command is sent to the SMP server on Duplex `subscribe Off`?**
|
||||
|
||||
A -
|
||||
|
||||
#### `getStatus`
|
||||
|
||||
#### `send`
|
||||
|
||||
#### `acknowledge`
|
||||
|
||||
#### `suspend`
|
||||
|
||||
#### `delete`
|
||||
|
||||
### Agent messages to user client
|
||||
|
||||
TODO
|
||||
|
||||
#### `connection`
|
||||
|
||||
#### `connectionInvitation`
|
||||
|
||||
#### `confirmation`
|
||||
|
||||
#### `message`
|
||||
|
||||
#### `unsubscribed`
|
||||
|
||||
#### `queueStatus`
|
||||
|
||||
#### `ok`
|
||||
|
||||
#### `error`
|
||||
|
||||
## Communication between SMP agents
|
||||
|
||||
TODO
|
||||
|
||||
#### `helloMsg`
|
||||
|
||||
#### `replyQueueMsg`
|
||||
|
||||
#### `deleteQueueMsg`
|
||||
|
||||
#### `clientMsg`
|
||||
|
||||
#### `acknowledgeMsg`
|
||||
|
||||
## General questions
|
||||
|
||||
- **Q - When does agent set up server connection groups?**
|
||||
|
||||
A - When it sees the new hostname:port combination - see the map of servers inside [AgentClient](https://github.com/simplex-chat/simplex-messaging/blob/client/src/Simplex/Messaging/Agent/Env.hs#L33)
|
||||
|
||||
- **Q - Replace command keywords with command names in the Duplex protocol sequence diagram?**
|
||||
|
||||
A - Let's keep keywords, I also use them as constructors in code, they are part of the actual command syntax.
|
||||
@@ -0,0 +1,79 @@
|
||||
digraph SMPAgent {
|
||||
graph [fontname=arial]
|
||||
node [fontname=arial fontsize=11 shape=box]
|
||||
edge [fontname=arial fontsize=10 arrowhead=open arrowtail=open]
|
||||
|
||||
subgraph clusterAgent {
|
||||
label="agent threads"
|
||||
main [shape=hexagon color=orange label="main\nthread"]
|
||||
aSock [label="user agent TCP socket" color=darkgreen]
|
||||
subgraph clusterThreads {
|
||||
label=""
|
||||
node [shape=hexagon color=orange]
|
||||
runUser [label="runUser\nthread"]
|
||||
runServer [label="runServer\nthread"]
|
||||
msgSubscriber [label="message\nsubscriber\nthread"]
|
||||
}
|
||||
main -> {runUser runServer} [style=dashed label=race color=orange fontcolor=orange]
|
||||
aSock -> runUser [color=darkgreen]
|
||||
|
||||
acInq [shape=larrow label="agent\nserver\ncommands\nTBQueue"]
|
||||
acInq -> runServer [color=darkgreen]
|
||||
runServer -> amInq [color=darkgreen label="unknown\nconnection\nor\nwrong\nconnection\nstate"]
|
||||
|
||||
amInq [shape=rarrow label="agent\nserver\nmessages\nTBQueue"]
|
||||
amInq -> msgSubscriber [color=blue]
|
||||
|
||||
msgSubscriber -> connectionsStore [dir=both]
|
||||
runUser -> connectionsStore [dir=both]
|
||||
runServer -> connectionsStore [dir=both]
|
||||
|
||||
connectionsStore [shape=cylinder label="duplex\nconnection\nrecords"]
|
||||
}
|
||||
|
||||
subgraph clusterUser {
|
||||
label="1 group per user connection"
|
||||
uSock [label="user connection TCP socket" color=darkgreen]
|
||||
|
||||
subgraph clusterUserThreads {
|
||||
graph [fontsize=11]
|
||||
label="user threads"
|
||||
node [shape=hexagon, color=orange]
|
||||
uAgent [label="user"]
|
||||
uSnd [label="send"]
|
||||
}
|
||||
|
||||
runUser -> uSock [style=dashed label="connect" color=darkgreen fontcolor=darkgreen]
|
||||
runUser -> {uAgent uSnd} [style=dashed label=race color=orange fontcolor=orange]
|
||||
|
||||
uOutq [shape=larrow label="send\nTBQueue"]
|
||||
uSock -> uAgent [color=darkgreen]
|
||||
uAgent -> uOutq [color=darkgreen label="SMAP\ncommand\nsyntax\nerror"]
|
||||
uOutq -> uSnd -> uSock [color=darkgreen]
|
||||
|
||||
msgSubscriber -> uOutq [color=blue label="SMAP\nmessages\nand\ncommand\nresponses"]
|
||||
uAgent -> acInq [color=darkgreen label="SMAP\nprotocol\ncommand"]
|
||||
}
|
||||
|
||||
subgraph clusterServer {
|
||||
label="1 group per SMP server connection"
|
||||
sSock [label="SMP server connection TCP socket" color=blue]
|
||||
|
||||
subgraph clusterServerThreads {
|
||||
graph [fontsize=11]
|
||||
label="server threads"
|
||||
node [shape=hexagon, color=orange]
|
||||
sAgent [label="server"]
|
||||
sSnd [label="send"]
|
||||
}
|
||||
|
||||
runServer -> {sAgent sSnd} [style=dashed label=race color=orange fontcolor=orange]
|
||||
|
||||
sOutq [shape=larrow label="send\nTBQueue"]
|
||||
sSock -> sAgent [color=blue]
|
||||
runServer -> sOutq [color=darkgreen label="SMP\nprotocol\ncommand"]
|
||||
sOutq -> sSnd -> sSock [color=blue]
|
||||
|
||||
sAgent -> amInq [color=blue label="wrapped\nSMP\nmessages\nand\ncommand\nresponses"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
digraph SMPAgent {
|
||||
graph [fontname=arial]
|
||||
node [fontname=arial fontsize=11 shape=box]
|
||||
edge [fontname=arial fontsize=10 arrowhead=open arrowtail=open]
|
||||
|
||||
main [shape=hexagon color=orange label="main\nthread"]
|
||||
aSock [label="user agent TCP socket" color=darkgreen]
|
||||
|
||||
subgraph clusterPersistence {
|
||||
graph [fontsize=11 color=gray]
|
||||
label="persistence (sqlite)"
|
||||
connectionsStore [shape=cylinder label="duplex connections,\nSMP queues,\nrecent messages"]
|
||||
}
|
||||
|
||||
subgraph clusterAgent {
|
||||
graph [color=gray]
|
||||
node [shape=hexagon color=orange]
|
||||
label="agent threads"
|
||||
connectClnt
|
||||
runClnt
|
||||
}
|
||||
|
||||
main -> {runClnt connectClnt} [style=dashed label="race" color=orange fontcolor=orange]
|
||||
aSock -> connectClnt [color=darkgreen]
|
||||
|
||||
subgraph clusterUserTCP {
|
||||
graph [color=gray]
|
||||
label="1 group per user TCP connection"
|
||||
uSock [label="user connection TCP socket" color=darkgreen]
|
||||
|
||||
subgraph clusterUserTCPThreads {
|
||||
graph [fontsize=11 color=gray]
|
||||
label="user TCP threads"
|
||||
node [shape=hexagon, color=orange]
|
||||
uRcv [label="user\nreceive"]
|
||||
uSnd [label="user\nsend"]
|
||||
}
|
||||
|
||||
uSock -> uRcv [color=darkgreen]
|
||||
uSnd -> uSock [color=darkgreen]
|
||||
}
|
||||
|
||||
subgraph clusterUser {
|
||||
graph [color=gray]
|
||||
label="1 group per user TCP connection"
|
||||
|
||||
subgraph clusterUserInterface {
|
||||
graph [fontsize=11]
|
||||
label="user queues"
|
||||
uInq [shape=rarrow label="user\nreceive\nTBQueue"]
|
||||
uOutq [shape=larrow label="user\nsend\nTBQueue"]
|
||||
}
|
||||
|
||||
subgraph clusterUserThreads {
|
||||
graph [fontsize=11]
|
||||
label="user threads\nNote: `user agent` sends\nall commands to `commands TBQueue`s\n(invalid commands with attached responses),\nand only valid commands to `server TBQueue`.\nIt is used to respond in correct order."
|
||||
node [shape=hexagon, color=orange]
|
||||
uAgent [label="user\nagent"]
|
||||
uProcess [label="process\nresponses"]
|
||||
}
|
||||
|
||||
uRcv -> uInq -> uAgent [color=darkgreen]
|
||||
uProcess -> uOutq -> uSnd [color=darkgreen]
|
||||
{uRcv uAgent} -> uOutq [color=green]
|
||||
|
||||
uRespq [shape=rarrow label="user\nSMP\nTBQueue"]
|
||||
|
||||
subgraph clusterClient {
|
||||
label="1 group per SMP client/server connection"
|
||||
runClient [shape=hexagon, color=orange]
|
||||
sOutq [shape=rarrow label="srv send\nTBQueue"]
|
||||
sSock [label="SMP client connection TCP socket" color=blue]
|
||||
|
||||
subgraph clusterServerThreads {
|
||||
graph [fontsize=11]
|
||||
label="SMP client threads"
|
||||
node [shape=hexagon, color=orange]
|
||||
sAgent [label="server\nreceive"]
|
||||
sSnd [label="server\nsend"]
|
||||
}
|
||||
|
||||
runClient -> {sAgent sSnd} [style=dashed label=race color=orange fontcolor=orange]
|
||||
{uAgent uProcess} -> sOutq -> sSnd -> sSock -> sAgent [color=blue]
|
||||
}
|
||||
|
||||
userState [label="connected\nservers,\nsubscribed\nqueues,\nsent\ncommands\n(STM)" shape="folder"]
|
||||
|
||||
{uAgent uProcess} -> userState [dir=both color="#FF8888"]
|
||||
{uAgent uProcess} -> connectionsStore [dir=both color="#880000"]
|
||||
|
||||
connectClnt -> uSock [style=dashed label="connect" color=darkgreen fontcolor=darkgreen]
|
||||
connectClnt -> {uRcv uSnd} [style=dashed label="race" color=orange fontcolor=orange]
|
||||
runClnt -> {uAgent uProcess} [style=dashed label="race" color=orange fontcolor=orange]
|
||||
|
||||
sAgent -> uRespq -> uProcess
|
||||
|
||||
uAgent -> runClient [style=dashed label="fork" color=orange fontcolor=orange]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: SMPAgent Pages: 1 -->
|
||||
<svg width="1073pt" height="1142pt"
|
||||
viewBox="0.00 0.00 1073.00 1142.34" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1138.3374)">
|
||||
<title>SMPAgent</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1138.3374 1069,-1138.3374 1069,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>clusterPersistence</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="927,-512.7374 927,-615.3874 1057,-615.3874 1057,-512.7374 927,-512.7374"/>
|
||||
<text text-anchor="middle" x="992" y="-601.4874" font-family="arial" font-size="11.00" fill="#000000">persistence (sqlite)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>clusterAgent</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="175,-963.3624 175,-1040.1624 389,-1040.1624 389,-963.3624 175,-963.3624"/>
|
||||
<text text-anchor="middle" x="282" y="-1023.5624" font-family="arial" font-size="14.00" fill="#000000">agent threads</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>clusterUserTCP</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="8,-509.375 8,-933.3624 230,-933.3624 230,-509.375 8,-509.375"/>
|
||||
<text text-anchor="middle" x="119" y="-916.7624" font-family="arial" font-size="14.00" fill="#000000">1 group per user TCP connection</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>clusterUserTCPThreads</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="38,-517.375 38,-610.7499 222,-610.7499 222,-517.375 38,-517.375"/>
|
||||
<text text-anchor="middle" x="130" y="-596.8499" font-family="arial" font-size="11.00" fill="#000000">user TCP threads</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>clusterUser</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="250,-8 250,-826.5624 919,-826.5624 919,-8 250,-8"/>
|
||||
<text text-anchor="middle" x="584.5" y="-809.9624" font-family="arial" font-size="14.00" fill="#000000">1 group per user TCP connection</text>
|
||||
</g>
|
||||
<g id="clust7" class="cluster">
|
||||
<title>clusterUserInterface</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="258,-400.375 258,-485.175 419,-485.175 419,-400.375 258,-400.375"/>
|
||||
<text text-anchor="middle" x="338.5" y="-471.275" font-family="arial" font-size="11.00" fill="#000000">user queues</text>
|
||||
</g>
|
||||
<g id="clust8" class="cluster">
|
||||
<title>clusterUserThreads</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="622,-634.3874 622,-793.7624 865,-793.7624 865,-634.3874 622,-634.3874"/>
|
||||
<text text-anchor="middle" x="743.5" y="-779.8624" font-family="arial" font-size="11.00" fill="#000000">user threads</text>
|
||||
<text text-anchor="middle" x="743.5" y="-766.6624" font-family="arial" font-size="11.00" fill="#000000">Note: `user agent` sends</text>
|
||||
<text text-anchor="middle" x="743.5" y="-753.4624" font-family="arial" font-size="11.00" fill="#000000">all commands to `commands TBQueue`s</text>
|
||||
<text text-anchor="middle" x="743.5" y="-740.2624" font-family="arial" font-size="11.00" fill="#000000">(invalid commands with attached responses),</text>
|
||||
<text text-anchor="middle" x="743.5" y="-727.0624" font-family="arial" font-size="11.00" fill="#000000">and only valid commands to `server TBQueue`.</text>
|
||||
<text text-anchor="middle" x="743.5" y="-713.8624" font-family="arial" font-size="11.00" fill="#000000">It is used to respond in correct order.</text>
|
||||
</g>
|
||||
<g id="clust10" class="cluster">
|
||||
<title>clusterClient</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="427,-128.6 427,-482.975 812,-482.975 812,-128.6 427,-128.6"/>
|
||||
<text text-anchor="middle" x="619.5" y="-466.375" font-family="arial" font-size="14.00" fill="#000000">1 group per SMP client/server connection</text>
|
||||
</g>
|
||||
<g id="clust11" class="cluster">
|
||||
<title>clusterServerThreads</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="564,-136.6 564,-229.975 758,-229.975 758,-136.6 564,-136.6"/>
|
||||
<text text-anchor="middle" x="661" y="-216.075" font-family="arial" font-size="11.00" fill="#000000">SMP client threads</text>
|
||||
</g>
|
||||
<!-- main -->
|
||||
<g id="node1" class="node">
|
||||
<title>main</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="382.542,-1106.2499 363.271,-1134.4251 324.729,-1134.4251 305.458,-1106.2499 324.729,-1078.0747 363.271,-1078.0747 382.542,-1106.2499"/>
|
||||
<text text-anchor="middle" x="344" y="-1109.5499" font-family="arial" font-size="11.00" fill="#000000">main</text>
|
||||
<text text-anchor="middle" x="344" y="-1096.3499" font-family="arial" font-size="11.00" fill="#000000">thread</text>
|
||||
</g>
|
||||
<!-- connectClnt -->
|
||||
<g id="node4" class="node">
|
||||
<title>connectClnt</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="288.8246,-989.3624 262.4123,-1007.3624 209.5877,-1007.3624 183.1754,-989.3624 209.5877,-971.3624 262.4123,-971.3624 288.8246,-989.3624"/>
|
||||
<text text-anchor="middle" x="236" y="-986.0624" font-family="arial" font-size="11.00" fill="#000000">connectClnt</text>
|
||||
</g>
|
||||
<!-- main->connectClnt -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>main->connectClnt</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M321.6223,-1082.0307C303.5136,-1062.4318 278.0793,-1034.9044 259.6406,-1014.9484"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="252.7011,-1007.4378 262.7926,-1011.7288 256.0942,-1011.1102 259.4874,-1014.7826 259.4874,-1014.7826 259.4874,-1014.7826 256.0942,-1011.1102 256.1823,-1017.8365 252.7011,-1007.4378 252.7011,-1007.4378"/>
|
||||
<text text-anchor="middle" x="308.7235" y="-1051.1624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- runClnt -->
|
||||
<g id="node5" class="node">
|
||||
<title>runClnt</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="380.5019,-989.3624 362.251,-1007.3624 325.749,-1007.3624 307.4981,-989.3624 325.749,-971.3624 362.251,-971.3624 380.5019,-989.3624"/>
|
||||
<text text-anchor="middle" x="344" y="-986.0624" font-family="arial" font-size="11.00" fill="#000000">runClnt</text>
|
||||
</g>
|
||||
<!-- main->runClnt -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>main->runClnt</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M344,-1077.9547C344,-1059.6552 344,-1035.9253 344,-1017.631"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="344,-1007.5699 348.5001,-1017.5699 344,-1012.5699 344.0001,-1017.5699 344.0001,-1017.5699 344.0001,-1017.5699 344,-1012.5699 339.5001,-1017.57 344,-1007.5699 344,-1007.5699"/>
|
||||
<text text-anchor="middle" x="353.7235" y="-1051.1624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- aSock -->
|
||||
<g id="node2" class="node">
|
||||
<title>aSock</title>
|
||||
<polygon fill="none" stroke="#006400" points="287.8624,-1124.2499 160.1376,-1124.2499 160.1376,-1088.2499 287.8624,-1088.2499 287.8624,-1124.2499"/>
|
||||
<text text-anchor="middle" x="224" y="-1102.9499" font-family="arial" font-size="11.00" fill="#000000">user agent TCP socket</text>
|
||||
</g>
|
||||
<!-- aSock->connectClnt -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>aSock->connectClnt</title>
|
||||
<path fill="none" stroke="#006400" d="M225.8487,-1088.2424C227.781,-1069.421 230.8399,-1039.6253 233.0952,-1017.6565"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="234.1294,-1007.583 237.5845,-1017.9903 233.6188,-1012.5568 233.1081,-1017.5307 233.1081,-1017.5307 233.1081,-1017.5307 233.6188,-1012.5568 228.6316,-1017.0711 234.1294,-1007.583 234.1294,-1007.583"/>
|
||||
</g>
|
||||
<!-- connectionsStore -->
|
||||
<g id="node3" class="node">
|
||||
<title>connectionsStore</title>
|
||||
<path fill="none" stroke="#000000" d="M1048.7017,-580.4228C1048.7017,-583.7287 1023.2871,-586.414 992,-586.414 960.7129,-586.414 935.2983,-583.7287 935.2983,-580.4228 935.2983,-580.4228 935.2983,-526.5021 935.2983,-526.5021 935.2983,-523.1962 960.7129,-520.5109 992,-520.5109 1023.2871,-520.5109 1048.7017,-523.1962 1048.7017,-526.5021 1048.7017,-526.5021 1048.7017,-580.4228 1048.7017,-580.4228"/>
|
||||
<path fill="none" stroke="#000000" d="M1048.7017,-580.4228C1048.7017,-577.117 1023.2871,-574.4316 992,-574.4316 960.7129,-574.4316 935.2983,-577.117 935.2983,-580.4228"/>
|
||||
<text text-anchor="middle" x="992" y="-563.3624" font-family="arial" font-size="11.00" fill="#000000">duplex connections,</text>
|
||||
<text text-anchor="middle" x="992" y="-550.1624" font-family="arial" font-size="11.00" fill="#000000">SMP queues,</text>
|
||||
<text text-anchor="middle" x="992" y="-536.9624" font-family="arial" font-size="11.00" fill="#000000">recent messages</text>
|
||||
</g>
|
||||
<!-- uSock -->
|
||||
<g id="node6" class="node">
|
||||
<title>uSock</title>
|
||||
<polygon fill="none" stroke="#006400" points="169.5337,-900.5624 16.4663,-900.5624 16.4663,-864.5624 169.5337,-864.5624 169.5337,-900.5624"/>
|
||||
<text text-anchor="middle" x="93" y="-879.2624" font-family="arial" font-size="11.00" fill="#000000">user connection TCP socket</text>
|
||||
</g>
|
||||
<!-- connectClnt->uSock -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>connectClnt->uSock</title>
|
||||
<path fill="none" stroke="#006400" stroke-dasharray="5,2" d="M198.6778,-978.637C182.4066,-972.8176 163.7253,-964.4984 148.985,-953.3624 133.1431,-941.3941 119.2685,-923.9297 109.2945,-909.3413"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="103.6056,-900.7046 112.8644,-906.5804 106.356,-904.8801 109.1064,-909.0557 109.1064,-909.0557 109.1064,-909.0557 106.356,-904.8801 105.3483,-911.5311 103.6056,-900.7046 103.6056,-900.7046"/>
|
||||
<text text-anchor="middle" x="165.5075" y="-944.3624" font-family="arial" font-size="10.00" fill="#006400">connect</text>
|
||||
</g>
|
||||
<!-- uRcv -->
|
||||
<g id="node7" class="node">
|
||||
<title>uRcv</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="214.0134,-553.4624 193.0067,-581.6377 150.9933,-581.6377 129.9866,-553.4624 150.9933,-525.2872 193.0067,-525.2872 214.0134,-553.4624"/>
|
||||
<text text-anchor="middle" x="172" y="-556.7624" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="172" y="-543.5624" font-family="arial" font-size="11.00" fill="#000000">receive</text>
|
||||
</g>
|
||||
<!-- connectClnt->uRcv -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>connectClnt->uRcv</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M237.1238,-971.0203C239.9206,-916.7089 244.541,-752.4513 208,-623.3874 204.8674,-612.323 199.8997,-600.9707 194.6434,-590.7174"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="189.7794,-581.6499 198.472,-588.3349 192.143,-586.056 194.5065,-590.4621 194.5065,-590.4621 194.5065,-590.4621 192.143,-586.056 190.541,-592.5893 189.7794,-581.6499 189.7794,-581.6499"/>
|
||||
<text text-anchor="middle" x="247.7235" y="-837.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uSnd -->
|
||||
<g id="node8" class="node">
|
||||
<title>uSnd</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="111.5662,-553.4624 95.2831,-581.6377 62.7169,-581.6377 46.4338,-553.4624 62.7169,-525.2872 95.2831,-525.2872 111.5662,-553.4624"/>
|
||||
<text text-anchor="middle" x="79" y="-556.7624" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="79" y="-543.5624" font-family="arial" font-size="11.00" fill="#000000">send</text>
|
||||
</g>
|
||||
<!-- connectClnt->uSnd -->
|
||||
<g id="edge25" class="edge">
|
||||
<title>connectClnt->uSnd</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M232.6954,-971.3264C227.2453,-941.3316 216.1632,-879.2768 208,-826.5624 201.0253,-781.5225 212.2627,-655.5799 180,-623.3874 161.268,-604.6963 143.493,-629.327 121,-615.3874 111.1934,-609.31 103.1361,-599.9964 96.7947,-590.4234"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="91.4524,-581.6443 100.4951,-587.8476 94.0516,-585.9157 96.6509,-590.187 96.6509,-590.187 96.6509,-590.187 94.0516,-585.9157 92.8067,-592.5263 91.4524,-581.6443 91.4524,-581.6443"/>
|
||||
<text text-anchor="middle" x="219.7235" y="-837.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uAgent -->
|
||||
<g id="node11" class="node">
|
||||
<title>uAgent</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="715.5622,-670.4749 697.7811,-698.6502 662.2189,-698.6502 644.4378,-670.4749 662.2189,-642.2997 697.7811,-642.2997 715.5622,-670.4749"/>
|
||||
<text text-anchor="middle" x="680" y="-673.7749" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="680" y="-660.5749" font-family="arial" font-size="11.00" fill="#000000">agent</text>
|
||||
</g>
|
||||
<!-- runClnt->uAgent -->
|
||||
<g id="edge26" class="edge">
|
||||
<title>runClnt->uAgent</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M363.0275,-971.304C418.9032,-918.2741 582.8091,-762.7159 650.3686,-698.5972"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="657.7387,-691.6025 653.5831,-701.7505 654.112,-695.0444 650.4853,-698.4864 650.4853,-698.4864 650.4853,-698.4864 654.112,-695.0444 647.3875,-695.2224 657.7387,-691.6025 657.7387,-691.6025"/>
|
||||
<text text-anchor="middle" x="482.7235" y="-879.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uProcess -->
|
||||
<g id="node12" class="node">
|
||||
<title>uProcess</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="849.4801,-670.4749 822.24,-698.6502 767.76,-698.6502 740.5199,-670.4749 767.76,-642.2997 822.24,-642.2997 849.4801,-670.4749"/>
|
||||
<text text-anchor="middle" x="795" y="-673.7749" font-family="arial" font-size="11.00" fill="#000000">process</text>
|
||||
<text text-anchor="middle" x="795" y="-660.5749" font-family="arial" font-size="11.00" fill="#000000">responses</text>
|
||||
</g>
|
||||
<!-- runClnt->uProcess -->
|
||||
<g id="edge27" class="edge">
|
||||
<title>runClnt->uProcess</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M375.4171,-984.2599C446.9624,-971.0733 623.6511,-929.4235 725,-826.5624 757.4412,-793.6372 776.3095,-743.0546 786.1177,-708.4539"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="788.7618,-698.7069 790.4867,-709.5363 787.4527,-703.5325 786.1436,-708.3581 786.1436,-708.3581 786.1436,-708.3581 787.4527,-703.5325 781.8006,-707.1799 788.7618,-698.7069 788.7618,-698.7069"/>
|
||||
<text text-anchor="middle" x="686.7235" y="-879.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uSock->uRcv -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>uSock->uRcv</title>
|
||||
<path fill="none" stroke="#006400" d="M97.3599,-864.3998C109.8154,-812.5125 145.7537,-662.8 162.808,-591.7545"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="165.1822,-581.8642 167.2236,-592.6383 164.0151,-586.7261 162.848,-591.5879 162.848,-591.5879 162.848,-591.5879 164.0151,-586.7261 158.4723,-590.5375 165.1822,-581.8642 165.1822,-581.8642"/>
|
||||
</g>
|
||||
<!-- uInq -->
|
||||
<g id="node9" class="node">
|
||||
<title>uInq</title>
|
||||
<polygon fill="none" stroke="#000000" points="392.5723,-449.7766 347.4277,-449.7766 347.4277,-414.5733 392.5723,-414.5733 392.5723,-408.5733 410.5723,-432.175 392.5723,-455.7766 392.5723,-449.7766"/>
|
||||
<text text-anchor="middle" x="379" y="-442.075" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="379" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">receive</text>
|
||||
<text text-anchor="middle" x="379" y="-415.675" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uRcv->uInq -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>uRcv->uInq</title>
|
||||
<path fill="none" stroke="#006400" d="M207.752,-544.8492C243.6388,-534.9021 299.1678,-515.8485 339,-485.175 346.5794,-479.3383 353.4481,-471.7165 359.2704,-464.0802"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="365.1617,-455.8721 362.9865,-466.6201 362.2462,-459.9341 359.3307,-463.9961 359.3307,-463.9961 359.3307,-463.9961 362.2462,-459.9341 355.6749,-461.3722 365.1617,-455.8721 365.1617,-455.8721"/>
|
||||
</g>
|
||||
<!-- uOutq -->
|
||||
<g id="node10" class="node">
|
||||
<title>uOutq</title>
|
||||
<polygon fill="none" stroke="#000000" points="329.5723,-449.7766 284.4277,-449.7766 284.4277,-455.7766 266.4277,-432.175 284.4277,-408.5733 284.4277,-414.5733 329.5723,-414.5733 329.5723,-449.7766"/>
|
||||
<text text-anchor="middle" x="298" y="-442.075" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="298" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">send</text>
|
||||
<text text-anchor="middle" x="298" y="-415.675" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uRcv->uOutq -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>uRcv->uOutq</title>
|
||||
<path fill="none" stroke="#00ff00" d="M196.5932,-529.7891C216.4531,-510.6719 244.5472,-483.6285 266.1304,-462.8527"/>
|
||||
<polygon fill="#00ff00" stroke="#00ff00" points="273.4688,-455.7886 269.3851,-465.9658 269.8666,-459.2562 266.2643,-462.7237 266.2643,-462.7237 266.2643,-462.7237 269.8666,-459.2562 263.1435,-459.4817 273.4688,-455.7886 273.4688,-455.7886"/>
|
||||
</g>
|
||||
<!-- uSnd->uSock -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>uSnd->uSock</title>
|
||||
<path fill="none" stroke="#006400" d="M80.2082,-581.8642C82.8558,-644.1008 89.1434,-791.9036 91.7913,-854.15"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="92.2274,-864.3998 87.3063,-854.6001 92.0148,-859.4043 91.8022,-854.4088 91.8022,-854.4088 91.8022,-854.4088 92.0148,-859.4043 96.2982,-854.2175 92.2274,-864.3998 92.2274,-864.3998"/>
|
||||
</g>
|
||||
<!-- uInq->uAgent -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>uInq->uAgent</title>
|
||||
<path fill="none" stroke="#006400" d="M396.6501,-455.9955C404.2553,-465.5484 413.5612,-476.3588 423,-485.175 495.6201,-553.0045 593.7284,-617.3782 645.1887,-649.3958"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="653.9378,-654.8067 643.0659,-653.374 649.6853,-652.1767 645.4329,-649.5468 645.4329,-649.5468 645.4329,-649.5468 649.6853,-652.1767 647.7999,-645.7196 653.9378,-654.8067 653.9378,-654.8067"/>
|
||||
</g>
|
||||
<!-- uOutq->uSnd -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>uOutq->uSnd</title>
|
||||
<path fill="none" stroke="#006400" d="M266.3235,-441.7172C229.3231,-453.7931 167.2864,-476.9926 121,-509.375 115.6566,-513.1132 110.4497,-517.6098 105.6237,-522.2822"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="98.3887,-529.6856 102.1596,-519.3885 101.8833,-526.1096 105.378,-522.5337 105.378,-522.5337 105.378,-522.5337 101.8833,-526.1096 108.5964,-525.6789 98.3887,-529.6856 98.3887,-529.6856"/>
|
||||
</g>
|
||||
<!-- uAgent->connectionsStore -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>uAgent->connectionsStore</title>
|
||||
<path fill="none" stroke="#880000" d="M711.4852,-644.5965C717.9336,-640.4916 724.9231,-636.8075 732,-634.3874 772.2162,-620.6351 884.2913,-641.9405 923,-624.3874 937.8878,-617.6364 951.2411,-606.1048 962.1439,-594.2794"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="703.07,-650.3759 708.7656,-641.0051 707.1916,-647.5452 711.3132,-644.7146 711.3132,-644.7146 711.3132,-644.7146 707.1916,-647.5452 713.8608,-648.424 703.07,-650.3759 703.07,-650.3759"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="968.9461,-586.5133 965.7424,-597.0007 965.6517,-590.2745 962.3573,-594.0358 962.3573,-594.0358 962.3573,-594.0358 965.6517,-590.2745 958.9722,-591.0708 968.9461,-586.5133 968.9461,-586.5133"/>
|
||||
</g>
|
||||
<!-- uAgent->uOutq -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>uAgent->uOutq</title>
|
||||
<path fill="none" stroke="#00ff00" d="M647.3434,-665.3697C591.4951,-656.2129 480.4257,-636.0193 447,-615.3874 386.1848,-577.8496 339.1057,-506.4848 315.2537,-464.7649"/>
|
||||
<polygon fill="#00ff00" stroke="#00ff00" points="310.2445,-455.8352 319.0616,-462.3552 312.6907,-460.196 315.1369,-464.5567 315.1369,-464.5567 315.1369,-464.5567 312.6907,-460.196 311.2123,-466.7583 310.2445,-455.8352 310.2445,-455.8352"/>
|
||||
</g>
|
||||
<!-- runClient -->
|
||||
<g id="node14" class="node">
|
||||
<title>runClient</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="722.7244,-432.175 701.3622,-450.175 658.6378,-450.175 637.2756,-432.175 658.6378,-414.175 701.3622,-414.175 722.7244,-432.175"/>
|
||||
<text text-anchor="middle" x="680" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">runClient</text>
|
||||
</g>
|
||||
<!-- uAgent->runClient -->
|
||||
<g id="edge30" class="edge">
|
||||
<title>uAgent->runClient</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M680,-642.203C680,-596.3198 680,-506.1699 680,-460.3495"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="680,-450.2297 684.5001,-460.2297 680,-455.2297 680.0001,-460.2297 680.0001,-460.2297 680.0001,-460.2297 680,-455.2297 675.5001,-460.2298 680,-450.2297 680,-450.2297"/>
|
||||
<text text-anchor="middle" x="688.3335" y="-550.4624" font-family="arial" font-size="10.00" fill="#ffa500">fork</text>
|
||||
</g>
|
||||
<!-- sOutq -->
|
||||
<g id="node15" class="node">
|
||||
<title>sOutq</title>
|
||||
<polygon fill="none" stroke="#000000" points="785.5723,-444.175 740.4277,-444.175 740.4277,-420.175 785.5723,-420.175 785.5723,-414.175 803.5723,-432.175 785.5723,-450.175 785.5723,-444.175"/>
|
||||
<text text-anchor="middle" x="772" y="-435.475" font-family="arial" font-size="11.00" fill="#000000">srv send</text>
|
||||
<text text-anchor="middle" x="772" y="-422.275" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uAgent->sOutq -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>uAgent->sOutq</title>
|
||||
<path fill="none" stroke="#0000ff" d="M691.5879,-642.1215C694.8569,-633.6035 698.2645,-624.1836 701,-615.3874 716.6229,-565.151 708.4801,-548.8178 729,-500.375 735.1504,-485.8551 744.2149,-470.9073 752.4376,-458.7199"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="758.2226,-450.3792 756.2211,-461.1608 755.373,-454.4877 752.5234,-458.5962 752.5234,-458.5962 752.5234,-458.5962 755.373,-454.4877 748.8257,-456.0316 758.2226,-450.3792 758.2226,-450.3792"/>
|
||||
</g>
|
||||
<!-- userState -->
|
||||
<g id="node19" class="node">
|
||||
<title>userState</title>
|
||||
<polygon fill="none" stroke="#000000" points="890.7837,-482.5758 887.7837,-486.5758 866.7837,-486.5758 863.7837,-482.5758 821.2163,-482.5758 821.2163,-381.7742 890.7837,-381.7742 890.7837,-482.5758"/>
|
||||
<text text-anchor="middle" x="856" y="-468.475" font-family="arial" font-size="11.00" fill="#000000">connected</text>
|
||||
<text text-anchor="middle" x="856" y="-455.275" font-family="arial" font-size="11.00" fill="#000000">servers,</text>
|
||||
<text text-anchor="middle" x="856" y="-442.075" font-family="arial" font-size="11.00" fill="#000000">subscribed</text>
|
||||
<text text-anchor="middle" x="856" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">queues,</text>
|
||||
<text text-anchor="middle" x="856" y="-415.675" font-family="arial" font-size="11.00" fill="#000000">sent</text>
|
||||
<text text-anchor="middle" x="856" y="-402.475" font-family="arial" font-size="11.00" fill="#000000">commands</text>
|
||||
<text text-anchor="middle" x="856" y="-389.275" font-family="arial" font-size="11.00" fill="#000000">(STM)</text>
|
||||
</g>
|
||||
<!-- uAgent->userState -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>uAgent->userState</title>
|
||||
<path fill="none" stroke="#ff8888" d="M705.4839,-635.9704C734.1546,-597.1508 781.1323,-533.5442 814.9848,-487.7086"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="699.5075,-644.0622 701.8288,-633.3448 702.478,-640.0402 705.4485,-636.0182 705.4485,-636.0182 705.4485,-636.0182 702.478,-640.0402 709.0683,-638.6917 699.5075,-644.0622 699.5075,-644.0622"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="820.9337,-479.654 818.6124,-490.3714 817.9632,-483.6759 814.9927,-487.6979 814.9927,-487.6979 814.9927,-487.6979 817.9632,-483.6759 811.3729,-485.0245 820.9337,-479.654 820.9337,-479.654"/>
|
||||
</g>
|
||||
<!-- uProcess->connectionsStore -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>uProcess->connectionsStore</title>
|
||||
<path fill="none" stroke="#880000" d="M851.5097,-660.0083C892.5279,-651.4468 943.5859,-638.4864 960,-624.3874 968.7086,-616.9071 975.1618,-606.5771 979.8973,-596.1137"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="841.5933,-662.0345 850.49,-655.6236 846.4921,-661.0335 851.3909,-660.0325 851.3909,-660.0325 851.3909,-660.0325 846.4921,-661.0335 852.2918,-664.4414 841.5933,-662.0345 841.5933,-662.0345"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="983.7787,-586.5449 984.1898,-597.5031 981.8992,-591.1782 980.0198,-595.8116 980.0198,-595.8116 980.0198,-595.8116 981.8992,-591.1782 975.8498,-594.12 983.7787,-586.5449 983.7787,-586.5449"/>
|
||||
</g>
|
||||
<!-- uProcess->uOutq -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>uProcess->uOutq</title>
|
||||
<path fill="none" stroke="#006400" d="M782.4156,-642.109C763.1961,-600.1829 726.1195,-524.9349 701,-509.375 632.2716,-466.8022 409.1254,-523.6095 338,-485.175 329.0427,-480.3347 321.505,-472.5258 315.4734,-464.3728"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="309.7367,-455.862 319.0575,-461.639 312.5314,-460.0081 315.326,-464.1542 315.326,-464.1542 315.326,-464.1542 312.5314,-460.0081 311.5946,-466.6694 309.7367,-455.862 309.7367,-455.862"/>
|
||||
</g>
|
||||
<!-- uProcess->sOutq -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>uProcess->sOutq</title>
|
||||
<path fill="none" stroke="#0000ff" d="M792.2713,-642.203C787.8428,-596.3198 779.1418,-506.1699 774.7193,-460.3495"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="773.7426,-450.2297 779.1826,-459.7511 774.223,-455.2066 774.7034,-460.1835 774.7034,-460.1835 774.7034,-460.1835 774.223,-455.2066 770.2242,-460.6158 773.7426,-450.2297 773.7426,-450.2297"/>
|
||||
</g>
|
||||
<!-- uProcess->userState -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>uProcess->userState</title>
|
||||
<path fill="none" stroke="#ff8888" d="M804.7422,-632.4164C814.4107,-594.6461 829.3748,-536.188 840.6433,-492.1666"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="802.237,-642.203 800.3575,-631.3994 803.477,-637.3592 804.717,-632.5154 804.717,-632.5154 804.717,-632.5154 803.477,-637.3592 809.0764,-633.6313 802.237,-642.203 802.237,-642.203"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="843.1328,-482.4415 845.0123,-493.2451 841.8928,-487.2854 840.6529,-492.1292 840.6529,-492.1292 840.6529,-492.1292 841.8928,-487.2854 836.2934,-491.0132 843.1328,-482.4415 843.1328,-482.4415"/>
|
||||
</g>
|
||||
<!-- uRespq -->
|
||||
<g id="node13" class="node">
|
||||
<title>uRespq</title>
|
||||
<polygon fill="none" stroke="#000000" points="744.5723,-57.4017 699.4277,-57.4017 699.4277,-22.1983 744.5723,-22.1983 744.5723,-16.1983 762.5723,-39.8 744.5723,-63.4017 744.5723,-57.4017"/>
|
||||
<text text-anchor="middle" x="731" y="-49.7" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="731" y="-36.5" font-family="arial" font-size="11.00" fill="#000000">SMP</text>
|
||||
<text text-anchor="middle" x="731" y="-23.3" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uRespq->uProcess -->
|
||||
<g id="edge29" class="edge">
|
||||
<title>uRespq->uProcess</title>
|
||||
<path fill="none" stroke="#000000" d="M762.5896,-44.0035C813.9769,-51.7359 910,-70.3048 910,-101.1 910,-553.4624 910,-553.4624 910,-553.4624 910,-594.8589 872.935,-626.9659 840.89,-646.9888"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="832.0197,-652.3135 838.2775,-643.3085 836.3067,-649.7401 840.5936,-647.1667 840.5936,-647.1667 840.5936,-647.1667 836.3067,-649.7401 842.9097,-651.0249 832.0197,-652.3135 832.0197,-652.3135"/>
|
||||
</g>
|
||||
<!-- sAgent -->
|
||||
<g id="node17" class="node">
|
||||
<title>sAgent</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="656.0134,-172.6875 635.0067,-200.8627 592.9933,-200.8627 571.9866,-172.6875 592.9933,-144.5122 635.0067,-144.5122 656.0134,-172.6875"/>
|
||||
<text text-anchor="middle" x="614" y="-175.9875" font-family="arial" font-size="11.00" fill="#000000">server</text>
|
||||
<text text-anchor="middle" x="614" y="-162.7875" font-family="arial" font-size="11.00" fill="#000000">receive</text>
|
||||
</g>
|
||||
<!-- runClient->sAgent -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>runClient->sAgent</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M663.5617,-414.1714C650.0082,-397.4105 633,-371.0774 633,-344.475 633,-344.475 633,-344.475 633,-257.475 633,-242.0587 629.9721,-225.439 626.3339,-211.0334"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="623.6113,-200.9841 630.5697,-209.4594 624.9188,-205.8101 626.2263,-210.6361 626.2263,-210.6361 626.2263,-210.6361 624.9188,-205.8101 621.8829,-211.8129 623.6113,-200.9841 623.6113,-200.9841"/>
|
||||
<text text-anchor="middle" x="642.7235" y="-297.975" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- sSnd -->
|
||||
<g id="node18" class="node">
|
||||
<title>sSnd</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="750.0217,-172.6875 731.0109,-200.8627 692.9891,-200.8627 673.9783,-172.6875 692.9891,-144.5122 731.0109,-144.5122 750.0217,-172.6875"/>
|
||||
<text text-anchor="middle" x="712" y="-175.9875" font-family="arial" font-size="11.00" fill="#000000">server</text>
|
||||
<text text-anchor="middle" x="712" y="-162.7875" font-family="arial" font-size="11.00" fill="#000000">send</text>
|
||||
</g>
|
||||
<!-- runClient->sSnd -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>runClient->sSnd</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M684.2567,-414.1504C688.048,-396.5013 693,-368.7944 693,-344.475 693,-344.475 693,-344.475 693,-257.475 693,-242.0587 696.0279,-225.439 699.6661,-211.0334"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="702.3887,-200.9841 704.1171,-211.8129 701.0812,-205.8101 699.7737,-210.6361 699.7737,-210.6361 699.7737,-210.6361 701.0812,-205.8101 695.4303,-209.4594 702.3887,-200.9841 702.3887,-200.9841"/>
|
||||
<text text-anchor="middle" x="702.7235" y="-297.975" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- sOutq->sSnd -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>sOutq->sSnd</title>
|
||||
<path fill="none" stroke="#0000ff" d="M767.4159,-414.1688C763.3329,-396.5332 758,-368.8362 758,-344.475 758,-344.475 758,-344.475 758,-257.475 758,-238.8558 749.3238,-220.4171 739.5075,-205.5139"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="733.653,-197.1731 743.0814,-202.7728 736.5256,-201.2656 739.3981,-205.3581 739.3981,-205.3581 739.3981,-205.3581 736.5256,-201.2656 735.7149,-207.9434 733.653,-197.1731 733.653,-197.1731"/>
|
||||
</g>
|
||||
<!-- sSock -->
|
||||
<g id="node16" class="node">
|
||||
<title>sSock</title>
|
||||
<polygon fill="none" stroke="#0000ff" points="619.2006,-450.175 434.7994,-450.175 434.7994,-414.175 619.2006,-414.175 619.2006,-450.175"/>
|
||||
<text text-anchor="middle" x="527" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">SMP client connection TCP socket</text>
|
||||
</g>
|
||||
<!-- sSock->sAgent -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>sSock->sAgent</title>
|
||||
<path fill="none" stroke="#0000ff" d="M533.3267,-413.9457C538.8538,-396.3776 546,-368.9339 546,-344.475 546,-344.475 546,-344.475 546,-257.475 546,-234.2863 561.6699,-213.8223 577.9653,-198.8106"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="585.9492,-191.8912 581.3395,-201.8412 582.1708,-195.1659 578.3923,-198.4406 578.3923,-198.4406 578.3923,-198.4406 582.1708,-195.1659 575.4451,-195.04 585.9492,-191.8912 585.9492,-191.8912"/>
|
||||
</g>
|
||||
<!-- sAgent->uRespq -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>sAgent->uRespq</title>
|
||||
<path fill="none" stroke="#000000" d="M636.8365,-146.75C655.8728,-125.1288 683.0563,-94.254 703.3269,-71.2308"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="710.1839,-63.4427 706.9532,-73.9219 706.8798,-67.1955 703.5757,-70.9482 703.5757,-70.9482 703.5757,-70.9482 706.8798,-67.1955 700.1982,-67.9746 710.1839,-63.4427 710.1839,-63.4427"/>
|
||||
</g>
|
||||
<!-- sSnd->sSock -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>sSnd->sSock</title>
|
||||
<path fill="none" stroke="#0000ff" d="M693.7955,-200.8992C685.9738,-211.2174 676.0767,-222.2439 665,-229.975 634.5137,-251.2531 586,-220.2974 586,-257.475 586,-344.475 586,-344.475 586,-344.475 586,-368.2876 570.8403,-390.4932 555.8897,-406.6024"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="548.6344,-414.0012 552.4229,-403.7105 552.1352,-410.4312 555.6359,-406.8612 555.6359,-406.8612 555.6359,-406.8612 552.1352,-410.4312 558.8489,-410.0118 548.6344,-414.0012 548.6344,-414.0012"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 31 KiB |
+36
-5
@@ -1,8 +1,8 @@
|
||||
name: simplex-messaging
|
||||
version: 0.1.0.0
|
||||
name: simplexmq
|
||||
version: 0.3.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-messaging#readme
|
||||
homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
license: AGPL-3
|
||||
author: Evgeny Poberezkin
|
||||
maintainer: [email protected]
|
||||
@@ -12,18 +12,35 @@ extra-source-files:
|
||||
- README.md
|
||||
|
||||
dependencies:
|
||||
- ansi-terminal == 0.10.*
|
||||
- asn1-encoding == 0.9.*
|
||||
- asn1-types == 0.3.*
|
||||
- async == 2.2.*
|
||||
- attoparsec == 0.13.*
|
||||
- base >= 4.7 && < 5
|
||||
- base64-bytestring >= 1.0 && < 1.3
|
||||
- bytestring == 0.10.*
|
||||
- containers
|
||||
- cryptonite == 0.26.*
|
||||
- directory == 1.3.*
|
||||
- filepath == 1.4.*
|
||||
- generic-random == 1.3.*
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.15.*
|
||||
- mtl
|
||||
- network == 3.1.*
|
||||
- network-transport == 0.5.*
|
||||
- QuickCheck == 2.13.*
|
||||
- simple-logger == 0.1.*
|
||||
- sqlite-simple == 0.4.*
|
||||
- stm
|
||||
- template-haskell == 2.15.*
|
||||
- text == 1.2.*
|
||||
- time == 1.9.*
|
||||
- transformers == 0.5.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.1.*
|
||||
- x509 == 1.7.*
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
@@ -33,7 +50,18 @@ executables:
|
||||
source-dirs: apps/smp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
- cryptostore == 0.2.*
|
||||
- ini == 0.4.*
|
||||
- optparse-applicative == 0.15.*
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
smp-agent:
|
||||
source-dirs: apps/smp-agent
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
@@ -42,10 +70,13 @@ tests:
|
||||
source-dirs: tests
|
||||
main: Test.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
- simplexmq
|
||||
- hspec == 2.7.*
|
||||
- hspec-core == 2.7.*
|
||||
- HUnit == 1.6.*
|
||||
- random == 1.1.*
|
||||
- QuickCheck == 2.13.*
|
||||
- timeit == 2.0.*
|
||||
|
||||
ghc-options:
|
||||
# - -haddock
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
# SMP agent protocol - duplex communication over SMP protocol
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [SMP agent](#smp-agent)
|
||||
- [SMP agent protocol components](#smp-agent-protocol-components)
|
||||
- [Duplex connection procedure](#duplex-connection-procedure)
|
||||
- [Communication between SMP agents](#communication-between-smp-agents)
|
||||
- [Message syntax](#messages-between-smp-agents)
|
||||
- [HELLO message](#hello-message)
|
||||
- [REPLY message](#reply-message)
|
||||
- [MSG message](#msg-message)
|
||||
- [SMP agent commands](#smp-agent-commands)
|
||||
- [Client commands and server responses](#client-commands-and-server-responses)
|
||||
- [NEW command and INV response](#new-command-and-inv-response)
|
||||
- [JOIN command](#join-command)
|
||||
- [CON notification](#con-notification)
|
||||
- [SUB command](#sub-command)
|
||||
- [SEND command and SENT response](#send-command-and-sent-response)
|
||||
- [MSG notification](#msg-notification)
|
||||
- [END notification](#end-notification)
|
||||
- [OFF command](#off-command)
|
||||
- [DEL command](#del-command)
|
||||
- [Connection invitation](#connection-invitation)
|
||||
|
||||
## Abstract
|
||||
|
||||
The purpose of SMP agent protocol is to define the syntax and the semantics of communications between the client and the agent that connects to [SMP](./simplex-messaging.md) servers.
|
||||
|
||||
It provides:
|
||||
- convenient protocol to create and manage a bi-directional (duplex) connection to the users of SMP agents consisting of two separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections.
|
||||
- management of E2E encryption between SMP agents, generating ephemeral RSA keys for each connection.
|
||||
- SMP command authentication on SMP servers, generating ephemeral RSA keys for each SMP queue.
|
||||
- TCP transport handshake and encryption with SMP servers.
|
||||
- validation of message integrity.
|
||||
|
||||
SMP agent protocols provides no encryption or any security on the client side - it is assumed that the agent is executed in the trusted and secure environment.
|
||||
|
||||
The future versions of this protocol could provide:
|
||||
- managing redundant SMP queues with more than 1 queue in each direction.
|
||||
- managing simple symmetric groups as a foundation for chat groups and device synchronization.
|
||||
- agent cluster - synchronizing states of multiple agents.
|
||||
- secure "synchronous" streams with symmetric message encryption and connection-level authentication (requires extending [SMP protocol](./simplex-messaging.md)) - it can be used, e.g., for file transfers.
|
||||
|
||||
## SMP agent
|
||||
|
||||
SMP agent is a client-side process or library that communicates via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) with other SMP agents according to the commands received from its users. This protocol is a middle layer in SMP protocols stack (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections").
|
||||
|
||||
The agent must have a persistent storage to manage the states of known connections and of the client-side information of two SMP queues that each connection consists of, and also the buffer of the most recent messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hash and id of the last received message.
|
||||
|
||||
## SMP agent protocol components
|
||||
|
||||
SMP agent protocol has 3 main parts:
|
||||
|
||||
- the syntax and semantics of messages that SMP agents exchange between each other in order to:
|
||||
- negotiate establishing unidirectional (simplex) encrypted queues on SMP server(s)
|
||||
- exchange client messages and delivery notifications, providing sequential message IDs and message integrity (by including the hash of the previous message).
|
||||
- the syntax and semantics of the commands (a higher level interface than SMP protocol) that are sent over TCP or other sequential protocol by agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two simplex SMP queues.
|
||||
- the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including SMP server, queue ID and encryption key) to ensure [E2E encryption][1] the integrity of SMP queues and protection against active attacks ([MITM attacks][2]).
|
||||
|
||||
## Duplex connection procedure
|
||||
|
||||

|
||||
|
||||
The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection comprised of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps:
|
||||
|
||||
1. Alice requests the new connection from the SMP agent A using `NEW` command.
|
||||
2. Agent A creates an SMP queue on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection invitation](#connection-invitation).
|
||||
3. Alice sends the invitation to Bob via any secure channel they have (out-of-band message).
|
||||
4. Bob sends `JOIN` command with the invitation as a parameter to agent B to accept the connection.
|
||||
5. Establishing Alice's SMP queue (with SMP protocol commands):
|
||||
- Agent B sends unauthenticated message to SMP queue with ephemeral key that will be used to authenticate commands to the queue, as described in SMP protocol.
|
||||
- Agent A receives the KEY and secures the queue.
|
||||
- Agent B tries sending authenticated SMP SEND command with agent `HELLO` message until it succeeds. Once it succeeds, Bob's agent "knows" the queue is secured.
|
||||
6. Agent B creates a new SMP queue on the server.
|
||||
7. Establish Bob's SMP queue:
|
||||
- Agent B sends `REPLY` message with the invitation to this 2nd queue to Alice's agent (via the 1st queue).
|
||||
- Agent A having received this `REPLY` message sends unauthenticated message to SMP queue with Alice agent's ephemeral key that will be used to authenticate commands to the queue, as described in SMP protocol.
|
||||
- Bob's agent receives the key and secures the queue.
|
||||
- Alice's agent keeps sending `HELLO` message until it succeeds.
|
||||
8. Agents A and B notify Alice and Bob that connection is established.
|
||||
- Once sending `HELLO` succeeds, Alice's agent sends to Alice `CON` notification that confirms that now both parties can communicate.
|
||||
- Once Bob's agent receives `HELLO` from Alice's agent, it sends to Bob `CON` notification as well.
|
||||
|
||||
At this point the duplex connection between Alice and Bob is established, they can use `SEND` command to send messages. The diagram also shows how the connection status changes for both parties, where the first part is the status of the SMP queue to receive messages, and the second part - the status of the queue to send messages.
|
||||
|
||||
The most communication happens between the agents and servers, from the point of view of Alice and Bob they have only 3 steps to do:
|
||||
|
||||
1. Alice requests a new connection with `NEW` command and receives the invitation.
|
||||
2. Alice passes invitation out-of-band to Bob.
|
||||
3. Bob accepts the connection by sending `JOIN` command with the invitation to his agent.
|
||||
|
||||
## Communication between SMP agents
|
||||
|
||||
SMP agents communicate via SMP servers managing creation, deletion and operations of SMP queues.
|
||||
|
||||
Agents can use SMP message client body (the part of the SMP message after header - see [SMP protocol](./simplex-messaging.md)) to transmit agent client messages and exchange messages between each other.
|
||||
|
||||
Each SMP message client body, once decrypted, contains 3 parts (one of them may include binary message body), as defined by `decryptedSmpMessageBody` syntax:
|
||||
|
||||
- `agentMsgHeader` - agent message header that contains sequential agent message ID for a particular SMP queue, agent timestamp (ISO8601) and the hash of the previous message.
|
||||
- `agentMessage` - a command/message to the other SMP agent:
|
||||
- to establish the connection with two SMP queues (`helloMsg`, `replyQueueMsg`)
|
||||
- to send and to acknowledge user messages (`clientMsg`, `acknowledgeMsg`)
|
||||
- to notify another agent about queue deletion (`deleteQueueMsg`)
|
||||
- `msgPadding` - an optional message padding to make all SMP messages have consistent size as an additional privacy protection measure.
|
||||
|
||||
### Messages between SMP agents
|
||||
|
||||
Message syntax below uses [ABNF][3] with [case-sensitive strings extension][4].
|
||||
|
||||
```abnf
|
||||
decryptedSmpMessageBody = agentMsgHeader CRLF agentMessage CRLF msgPadding
|
||||
agentMsgHeader = agentMsgId SP agentTimestamp SP previousMsgHash
|
||||
agentMsgId = 1*DIGIT ; sequential agent message ID set by the sending agent
|
||||
|
||||
agentMessage = helloMsg / replyQueueMsg / deleteQueueMsg
|
||||
/ clientMsg / acknowledgeMsg
|
||||
|
||||
msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size)
|
||||
|
||||
helloMsg = %s"HELLO" SP signatureVerificationKey [SP %s"NO_ACK"]
|
||||
; NO_ACK means that acknowledgements to client messages will NOT be sent in this connection by the agent that sent `HELLO` message.
|
||||
signatureVerificationKey = encoded ; base64 encoded
|
||||
|
||||
replyQueueMsg = %s"REPLY" SP qInfo ; `qInfo` is the same as in out-of-band message
|
||||
; this message can only be sent by the second connection party
|
||||
|
||||
deleteQueueMsg = %s"DEL" ; notification that recipient queue will be deleted
|
||||
; no need to notify the other party about suspending queue separately, as suspended and deleted queues are the same to the sender
|
||||
; NOT SUPPORTED with the current implementation
|
||||
|
||||
clientMsg = %s"MSG" SP size CRLF clientMsgBody CRLF ; CRLF is in addition to CRLF in decryptedSmpMessageBody
|
||||
size = 1*DIGIT
|
||||
clientMsgBody = *OCTET
|
||||
|
||||
acknowledgeMsg = %s"ACK" SP agentMsgId SP ackStatus
|
||||
; NOT SUPPORTED with the current implementation
|
||||
|
||||
ackStatus = %s"OK" / ackError
|
||||
|
||||
ackError = %s"ERR" SP ackErrorType
|
||||
|
||||
ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr
|
||||
|
||||
ackUnknownMsg = %s"UNKNOWN"
|
||||
|
||||
ackProhibitedMsg = %"PROHIBITED" ; e.g. "HELLO" or "REPLY"
|
||||
|
||||
ackSyntaxErr = %"SYNTAX" SP syntaxErrCode
|
||||
syntaxErrCode = 1*DIGIT ; TODO
|
||||
```
|
||||
|
||||
#### HELLO message
|
||||
|
||||
This is the first message that both agents send after the respective SMP queue is secured by the receiving agent (see diagram). It contains the verification key that the sender will use to cryptographically sign the messages.
|
||||
|
||||
Sending agent might need to retry sending HELLO message, as it would not have any other confirmation that the queue is secured other than the success of sending this message with the signed SEND command of SMP protocol.
|
||||
|
||||
#### REPLY message
|
||||
|
||||
This is the message that is sent by the agent that received an out-of-band invitation to pass the invitation to the reply SMP queue to the agent that originated the connection (see diagram).
|
||||
|
||||
#### MSG message
|
||||
|
||||
This is the agent envelope used to send client messages once the connection is established. Do not confuse it with the MSG response from SMP server to the agent and MSG response from SMP agent to the client that are sent in different contexts.
|
||||
|
||||
## SMP agent commands
|
||||
|
||||
This part describes the transmissions between users and client-side SMP agents: commands that the users send to create and operate duplex connections and SMP agent responses and messages they deliver.
|
||||
|
||||
Commands syntax below is provided using [ABNF][3] with [case-sensitive strings extension][4].
|
||||
|
||||
Each transmission between the user and SMP agent must have this format/syntax:
|
||||
|
||||
```abnf
|
||||
agentTransmission = [corrId] CRLF [cAlias] CRLF agentCommand
|
||||
|
||||
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
|
||||
|
||||
cAlias = cId / cName
|
||||
cId = encoded
|
||||
cName = 1*(ALPHA / DIGIT / "_" / "-")
|
||||
|
||||
agentCommand = (userCmd / agentMsg) CRLF
|
||||
userCmd = newCmd / joinCmd
|
||||
/ acceptCmd / subscribeCmd
|
||||
/ sendCmd / acknowledgeCmd
|
||||
/ suspendCmd / deleteCmd
|
||||
|
||||
agentMsg = invitation / confirmation
|
||||
/ connected / unsubscribed
|
||||
/ message / sent / received
|
||||
/ ok / error
|
||||
|
||||
newCmd = %s"NEW" SP smpServer [SP %s"NO_ACK"]
|
||||
; response is `invitation` or `error`
|
||||
smpServer = srvHost [":" port] ["#" keyFingerprint]
|
||||
srvHost = hostname ; RFC1123, RFC5891
|
||||
port = 1*DIGIT
|
||||
keyFingerprint = encoded
|
||||
|
||||
invitation = %s"INV" SP qInfo
|
||||
|
||||
connected = %s"CON"
|
||||
|
||||
subscribeCmd = %s"SUB" ; response is `ok` or `error`
|
||||
|
||||
unsubscribed = %s"END"
|
||||
; when another agent (or another client of the same agent)
|
||||
; subscribes to the same SMP queue on the server
|
||||
|
||||
joinCmd = %s"JOIN" SP qInfo
|
||||
[SP (smpServer / %s"NO_REPLY")] ; reply queue SMP server
|
||||
; server from qInfo is used by default
|
||||
[SP %s"NO_ACK"]
|
||||
; response is `connected` or `error`
|
||||
|
||||
confirmation = %s"CONF" SP partyId SP partyInfo
|
||||
; currently not implemented
|
||||
|
||||
acceptCmd = %s"LET" SP partyId ; response is `ok` or `error`
|
||||
; currently not implemented
|
||||
|
||||
suspendCmd = %s"OFF" ; can be sent by either party, response `ok` or `error`
|
||||
|
||||
deleteCmd = %s"DEL" ; can be sent by either party, response `ok` or `error`
|
||||
|
||||
sendCmd = %s"SEND" SP msgBody
|
||||
; send syntax is similar to that of SMP protocol, but it is wrapped in SMP message
|
||||
msgBody = stringMsg | binaryMsg
|
||||
stringMsg = ":" string ; until CRLF in the transmission
|
||||
string = *(%x01-09 / %x0B-0C / %x0E-FF %) ; any characters other than NUL, CR and LF
|
||||
binaryMsg = size CRLF msgBody CRLF ; the last CRLF is in addition to CRLF in the transmission
|
||||
size = 1*DIGIT ; size in bytes
|
||||
msgBody = *OCTET ; any content of specified size - safe for binary
|
||||
|
||||
sent = %s"SENT" SP agentMsgId
|
||||
|
||||
message = %s"MSG" SP msgIntegrity
|
||||
SP %s"R=" agentMsgId "," agentTimestamp ; receiving agent
|
||||
SP %s"B=" brokerMsgId "," srvTimestamp ; broker (server)
|
||||
SP %s"S=" agentMsgId "," agentTimestamp ; sending agent
|
||||
SP binaryMsg
|
||||
agentMsgId = 1*DIGIT
|
||||
srvTimestamp = date-time ; RFC3339
|
||||
agentTimestamp = date-time
|
||||
msgIntegrity = ok / messageError
|
||||
|
||||
messageError = %s"ERR" SP messageErrorType
|
||||
messageErrorType = skippedMsgErr / badMsgIdErr / badHashErr
|
||||
|
||||
skippedMsgErr = %"IDS" SP missingFromMsgId SP missingToMsgId
|
||||
badMsgIdErr = %"ID" SP previousMsgId ; ID is lower than the previous
|
||||
badHashErr = %"HASH"
|
||||
|
||||
acknowledge = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R")
|
||||
; currently not implemented
|
||||
|
||||
received = %s"RCVD" SP agentMsgId ; ID assigned by sending agent (in SENT response)
|
||||
; currently not implemented
|
||||
|
||||
ok = %s"OK"
|
||||
|
||||
error = %s"ERR" SP errorType
|
||||
|
||||
encoded = base64
|
||||
```
|
||||
|
||||
### Client commands and server responses
|
||||
|
||||
#### NEW command and INV response
|
||||
|
||||
`NEW` command is used to create a connection and an invitation to be sent out-of-band to another protocol user. It should be used by the client of the agent that initiates creating a duplex connection.
|
||||
|
||||
`INV` response is sent by the agent to the client.
|
||||
|
||||
#### JOIN command
|
||||
|
||||
It is used to create a connection and accept the invitation received out-of-band. It should be used by the client of the agent that accepts the connection.
|
||||
|
||||
#### CON notification
|
||||
|
||||
It is sent by both agents managing duplex connection to their clients once the connection is established and ready to accept client messages.
|
||||
|
||||
#### SUB command
|
||||
|
||||
This command can be used by the client to resume receiving messages from the connection that was created in another TCP/client session. Agent response to this command can be `OK` or `ERR` in case connection does not exist (or can only be used to send connections - e.g. when the reply queue was not created).
|
||||
|
||||
#### SEND command and SENT response
|
||||
|
||||
`SEND` command is used to the client to send messages
|
||||
|
||||
`SENT` response is sent by the agent to confirm that the message was delivered to the SMP server. Message ID in this response is the sequential message number that includes both sent and received messages in the connection.
|
||||
|
||||
#### MSG notification
|
||||
|
||||
It is sent by the agent to the client when agent receives the message from the SMP server. It has message ID and timestamp from both the receiving and sending agents and from SMP server:
|
||||
- recipient agent ID is intended to be used to refer to the message in the future.
|
||||
- sender agent ID is intended to be used to identify any missed / skipped message(s)
|
||||
- broker ID should be used to detect duplicate deliveries (it would happen if TCP connection is lost before the message is acknowledged by the agent - see [SMP protocol](./simplex-messaging.md))
|
||||
|
||||
#### END notification
|
||||
|
||||
It is sent by the agent to the client when agent receives SMP protocol `END` notification from SMP server. It indicates that another agent has subscribed to the same SMP queue on the server and the server terminated the subscription of the current agent.
|
||||
|
||||
#### OFF command
|
||||
|
||||
It is used to suspend the receiving SMP queue - sender will no longer be able to send the messages to the connection, but the recipient can retrieve the remaining messages. Agent response to this command can be `OK` or `ERR`. This command is irreversible.
|
||||
|
||||
#### DEL command
|
||||
|
||||
It is used to delete the connection and all messages in it, as well as the receiving SMP queue and all messages in it that were remaining on the server. Agent response to this command can be `OK` or `ERR`. This command is irreversible.
|
||||
|
||||
## Connection invitation
|
||||
|
||||
Connection invitation `qInfo` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another invitation is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users).
|
||||
|
||||
Connection invitation is a text with the following syntax:
|
||||
|
||||
```
|
||||
qInfo = %s"smp::" smpServer "::" queueId "::" ephemeralPublicKey
|
||||
queueId = encoded
|
||||
ephemeralPublicKey = %s"rsa:" encoded ; RSA key for sender to encrypt messages X509 base64 encoded
|
||||
```
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
[2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
||||
[3]: https://tools.ietf.org/html/rfc5234
|
||||
[4]: https://tools.ietf.org/html/rfc7405
|
||||
@@ -0,0 +1,71 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice
|
||||
participant AA as Alice's<br>agent
|
||||
participant AS as Alice's<br>server
|
||||
participant BS as Bob's<br>server
|
||||
participant BA as Bob's<br>agent
|
||||
participant B as Bob
|
||||
|
||||
note over AA, BA: status (receive/send): NONE/NONE
|
||||
|
||||
note over A, AA: 1. request connection from agent
|
||||
A ->> AA: NEW: create<br>duplex connection
|
||||
|
||||
note over AA, AS: 2. create Alice's SMP queue
|
||||
AA ->> AS: NEW: create SMP queue
|
||||
AS ->> AA: IDS: SMP queue IDs
|
||||
note over AA: status: NEW/NONE
|
||||
|
||||
AA ->> A: INV: invitation<br>to connect
|
||||
note over AA: status: PENDING/NONE
|
||||
|
||||
note over A, B: 3. out-of-band invitation
|
||||
A ->> B: OOB: invitation to connect
|
||||
|
||||
note over BA, B: 4. accept connection
|
||||
B ->> BA: JOIN:<br>via invitation info
|
||||
note over BA: status: NONE/NEW
|
||||
|
||||
note over BA, AA: 5. establish Alice's SMP queue
|
||||
BA ->> AS: SEND: KEY: sender's server key
|
||||
note over BA: status: NONE/CONFIRMED
|
||||
activate BA
|
||||
AS ->> AA: MSG: KEY: sender's server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: KEY: secure queue
|
||||
note over AA: status: SECURED/NONE
|
||||
|
||||
BA ->> AS: SEND: HELLO: try sending until successful
|
||||
deactivate BA
|
||||
note over BA: status: NONE/ACTIVE
|
||||
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
|
||||
note over AA: status: ACTIVE/NONE
|
||||
|
||||
note over BA, BS: 6. create Bob's SMP queue
|
||||
BA ->> BS: NEW: create SMP queue
|
||||
BS ->> BA: IDS: SMP queue IDs
|
||||
note over BA: status: NEW/ACTIVE
|
||||
|
||||
note over AA, BA: 7. establish Bob's SMP queue
|
||||
BA ->> AS: SEND: REPLY: invitation to the connect
|
||||
note over BA: status: PENDING/ACTIVE
|
||||
AS ->> AA: MSG: REPLY: invitation<br>to connect
|
||||
note over AA: status: ACTIVE/NEW
|
||||
|
||||
AA ->> BS: SEND: KEY: sender's server key
|
||||
note over AA: status: ACTIVE/CONFIRMED
|
||||
activate AA
|
||||
BS ->> BA: MSG: KEY: sender's server key
|
||||
note over BA: status: CONFIRMED/ACTIVE
|
||||
BA ->> BS: KEY: secure queue
|
||||
note over BA: status: SECURED/ACTIVE
|
||||
|
||||
AA ->> BS: SEND: HELLO: try sending until successful
|
||||
deactivate AA
|
||||
note over AA: status: ACTIVE/ACTIVE
|
||||
BS ->> BA: MSG: HELLO: Bob's agent<br>knows Alice can send
|
||||
note over BA: status: ACTIVE/ACTIVE
|
||||
|
||||
note over A, B: 8. notify users about connection success
|
||||
AA ->> A: CON: connected
|
||||
BA ->> B: CON: connected
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,20 @@
|
||||
sequenceDiagram
|
||||
participant B as Bob (sender)
|
||||
participant S as server (queue RID)
|
||||
participant A as Alice (recipient)
|
||||
|
||||
note over A: creating queue<br>("public" key RK<br>for msg retrieval)
|
||||
A ->> S: 1. create queue ("NEW")
|
||||
S ->> A: respond with queue RID and SID ("IDS")
|
||||
|
||||
note over A: out-of-band msg<br>(sender's queue SID<br>and "public" key EK<br>to encrypt msgs)
|
||||
A -->> B: 2. send out-of-band message
|
||||
|
||||
note over B: confirm queue<br>("public" key SK for<br>sending messages<br>and any optional<br>info encrypted with<br>"public" key EK)
|
||||
B ->> S: 3. confirm queue ("SEND" command not signed)
|
||||
|
||||
S ->> A: 4. deliver Bob's message
|
||||
note over A: decrypt message<br>("private" key EK)
|
||||
A ->> S: 5. secure queue ("KEY", RK-signed)
|
||||
|
||||
note over S: 6. simplex<br>queue RID<br>is ready to use!
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,26 @@
|
||||
sequenceDiagram
|
||||
participant S as sender (client)
|
||||
participant A as transport
|
||||
participant Q as pubsub
|
||||
participant P as transport
|
||||
participant R as receiver (client)
|
||||
note over R: sign subscription (1)
|
||||
R ->> P: subscribe to messages
|
||||
note over P: verify subscriber (1)
|
||||
alt subscriber verified?
|
||||
P -->> Q: subscribe
|
||||
else
|
||||
P ->> R: reject subscription
|
||||
end
|
||||
note over S: sign message (2)
|
||||
S ->> A: send message
|
||||
note over A: verify sender (2)
|
||||
alt sender verified?
|
||||
A -->> Q: queue message
|
||||
activate Q
|
||||
else
|
||||
A ->> S: reject message
|
||||
end
|
||||
Q -->> P: take message
|
||||
deactivate Q
|
||||
P ->> R: deliver message
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
||||
sequenceDiagram
|
||||
participant B as Bob (sender)
|
||||
participant S as server (queue RID)
|
||||
participant A as Alice (recipient)
|
||||
|
||||
note over B: encrypt message<br>("public" key EK)
|
||||
B ->> S: 1. send message to SID (SK-signed command)
|
||||
|
||||
S ->> A: 2. receive messages from RID (RK-signed subscription)
|
||||
note over A: decrypt message<br>("private" key EK)
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,11 @@
|
||||
graph LR
|
||||
VS{{"verify sender (SK)"}}
|
||||
VR{{"verify recipient (RK)"}}
|
||||
|
||||
S(sender) -->|msg| VS
|
||||
subgraph "server (queue RID, SID)"
|
||||
VS --> DB[("storage")]
|
||||
DB --> VR
|
||||
end
|
||||
R(recipient) -->|"1) sub"| VR
|
||||
VR -->|"2) msg"| R
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,624 @@
|
||||
# Simplex Messaging Protocol (SMP)
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [Introduction](#introduction)
|
||||
- [SMP Model](#smp-model)
|
||||
- [Out-of-band messages](#out-of-band-messages)
|
||||
- [Simplex queue](#simplex-queue)
|
||||
- [SMP procedure](#smp-procedure)
|
||||
- [SMP qualities and features](#smp-qualities-and-features)
|
||||
- [Cryptographic algorithms](#cryptographic-algorithms)
|
||||
- [Simplex queue IDs](#simplex-queue-ids)
|
||||
- [Server privacy requirements](#server-privacy-requirements)
|
||||
- [SMP commands](#smp-commands)
|
||||
- [Correlating responses with commands](#correlating-responses-with-commands)
|
||||
- [Command authentication](#command-authentication)
|
||||
- [Keep-alive command](#keep-alive-command)
|
||||
- [Recipient commands](#recipient-commands)
|
||||
- [Create queue command](#create-queue-command)
|
||||
- [Subscribe to queue](#subscribe-to-queue)
|
||||
- [Secure queue command](#secure-queue-command)
|
||||
- [Acknowledge message delivery](#acknowledge-message-delivery)
|
||||
- [Suspend queue](#suspend-queue)
|
||||
- [Delete queue](#delete-queue)
|
||||
- [Sender commands](#sender-commands)
|
||||
- [Send message](#send-message)
|
||||
- [Server messages](#server-messages)
|
||||
- [Queue IDs response](#queue-ids-response)
|
||||
- [Deliver queue message](#deliver-queue-message)
|
||||
- [Subscription END notification](#subscription-end-notification)
|
||||
- [Error responses](#error-responses)
|
||||
- [OK response](#ok-response)
|
||||
- [Appendices](#appendices)
|
||||
- [Appendix A. Transport connection with the SMP server](#appendix-a)
|
||||
|
||||
## Abstract
|
||||
|
||||
Simplex Messaging Protocol is a transport agnostic client-server protocol for asynchronous distributed secure unidirectional message transmission via persistent simplex message queues.
|
||||
|
||||
It's designed with the focus on communication security and integrity, under the assumption that any part of the message transmission network can be compromised.
|
||||
|
||||
It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system.
|
||||
|
||||
## Introduction
|
||||
|
||||
The objective of Simplex Messaging Protocol (SMP) is to facilitate the secure and private unidirectional transfer of messages from senders to recipients via persistent simplex queues managed by the message broker (server).
|
||||
|
||||
SMP is independent of any particular transmission system and requires only a reliable ordered data stream channel. While this document describes transport over TCP, other transports are also possible.
|
||||
|
||||
The protocol describes the set of commands that recipients and senders can exchange with SMP servers to create and to operate unidirectional "queues" (a data abstraction identifying one of many communication channels managed by the server) and to send messages from the sender to the recipient via the SMP server.
|
||||
|
||||
More complex communication scenarios can be designed using multiple queues - for example, a duplex communication channel can be made of 2 simplex queues.
|
||||
|
||||
The protocol is designed with the focus on privacy and security, to some extent deprioritizing reliability by requiring that SMP servers only store messages until they are acknowledged by the recipients and, in any case, for a limited period of time. For communication scenarios requiring more reliable transmission the users should use several SMP servers to pass each message and implement some additional protocol to ensure that messages are not removed, inserted or changed - this is out of scope of this document.
|
||||
|
||||
SMP does not use any form of participants' identities and provides [E2EE][2] without the possibility of [MITM attack][1] relying on two pre-requisites:
|
||||
|
||||
- the users can establish a secure encrypted transport connection with the SMP server. [Appendix A](#appendix-a) describes SMP transport protocol of such connection over TCP, but any other transport connection protocol can be used.
|
||||
|
||||
- the recipient can pass a single message to the sender via a pre-existing secure and private communication channel (out-of-band message) - the information in this message is used to encrypt messages and to establish connection with SMP server.
|
||||
|
||||
## SMP Model
|
||||
|
||||
The SMP model has three communication participants: the recipient, the message broker (SMP server) that is chosen and, possibly, controlled by the recipient, and the sender.
|
||||
|
||||
SMP server manages multiple "simplex queues" - data records on the server that identify communication channels from the senders to the recipients. The same communicating party that is the sender in one queue, can be the recipient in another - without exposing this fact to the server.
|
||||
|
||||
The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analysing their queues in case SMP server is compromised.
|
||||
|
||||
Creating and using the queue requires sending commands to the SMP server from the recipient and the sender - they are described in detail in [SMP commands](#smp-commands) section.
|
||||
|
||||
## Out-of-band messages
|
||||
|
||||
The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share the encryption (a.k.a. "public") key that the sender will use to encrypt the messages (to be decrypted by the recipient), sender queue ID, server hostname and any other information necessary to establish secure encrypted connection with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol).
|
||||
|
||||
The [ABNF][8] syntax of the message is:
|
||||
|
||||
```abnf
|
||||
outOfBandMsg = "smp::" server "::" queueId "::" encryptionKey
|
||||
server = <hostname> [":" port] ["#" serverKeyHash]
|
||||
port = 1*DIGIT
|
||||
serverKeyHash = encoded
|
||||
queueId = encoded
|
||||
encryptionKey = %s"rsa:" x509encoded ; the recipient's RSA public key for sender to encrypt messages
|
||||
x509encoded = <base64 X509 key encoding>
|
||||
encoded = <base64 encoded binary>
|
||||
```
|
||||
|
||||
`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1.
|
||||
|
||||
`port` is optional, the default TCP port for SMP protocol is 5223.
|
||||
|
||||
`serverKeyHash` is an optional hash of the server transport key used during transport handshake (see [Appendix A](#appendix-a)).
|
||||
|
||||
Encryption keys are encoded using [X509][11] specification.
|
||||
|
||||
Defining the approach to out-of-band message passing is out of scope of this protocol.
|
||||
|
||||
## Simplex queue
|
||||
|
||||
The simplex queue is the main unit of SMP protocol. It is used by:
|
||||
|
||||
- Sender of the queue (who received out-of-band message) to send messages to the server using sender's queue ID, signed by sender's key.
|
||||
|
||||
- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key.
|
||||
|
||||
- Participant identities are not shared with the server - new unique keys and queue IDs are used for each queue.
|
||||
|
||||
This simplex queue can serve as a building block for more complex communication network. For example, two (or more, for redundancy) simplex queues can be used to create a duplex communication channel. Higher level primitives that are only known to system participants in their client applications can be created as well - e.g., contacts, conversations, groups and broadcasts. Simplex messaging servers only have the information about the low-level simplex queues. In this way a high level of privacy and security of the communication is provided. Application level primitives are not in scope of this protocol.
|
||||
|
||||
This approach is based on the concept of [unidirectional networks][4] that are used for applications with high level of information security.
|
||||
|
||||
Access to each queue is controlled with unique (not shared with other queues) asymmetric key pairs, separate for the sender and the recipient. The sender and the receiver have private keys, and the server has associated public keys to authenticate participants' commands by verifying cryptographic signatures.
|
||||
|
||||
The messages sent over the queue are encrypted and decrypted using another key pair that was shared via out-of-band message - the recipient has the private key and the sender has the associated public key.
|
||||
|
||||
**Simplex queue diagram:**
|
||||
|
||||

|
||||
|
||||
Queue is defined by recipient ID `RID` and sender ID `SID`, unique for the server. Sender key (`SK`) is used by the server to verify sender's commands (identified by `SID`) to send messages. Recipient key (`RK`) is used by the server to verify recipient's commands (identified by `RID`) to retrieve messages.
|
||||
|
||||
The protocol uses different IDs for sender and recipient in order to provide an additional privacy by preventing the correlation of senders and recipients commands sent over the network - in case the encrypted transport is compromised, it would still be difficult to correlate senders and recipients without access to the queue records on the server.
|
||||
|
||||
## SMP procedure
|
||||
|
||||
The SMP procedure of creating a simplex queue on SMP server is explained using participants Alice (the recipient) who wants to receive messages from Bob (the sender).
|
||||
|
||||
To create and start using a simplex queue Alice and Bob follow these steps:
|
||||
|
||||
1. Alice creates a simplex queue on the server:
|
||||
|
||||
1. Decides which SMP server to use (can be the same or different server that Alice uses for other queues) and opens secure encrypted transport connection to the chosen SMP server (see [Appendix A](#appendix-a)).
|
||||
|
||||
2. Generates a new random public/private key pair (encryption key - `EK`) that she did not use before for Bob to encrypt the messages.
|
||||
|
||||
3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to sign commands and to decrypt the transmissions received from the server.
|
||||
|
||||
4. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" key `RK` that will be used to sign the following commands related to the same queue, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
|
||||
|
||||
5. The server sends `"IDS"` response with queue IDs (`queueIds`):
|
||||
|
||||
- Recipient ID `RID` for Alice to manage the queue and to receive the messages.
|
||||
|
||||
- Sender ID `SID` for Bob to send messages to the queue.
|
||||
|
||||
2. Alice sends an out-of-band message to Bob via the alternative channel that both Alice and Bob trust (see [protocol abstract](#simplex-messaging-protocol-abstract)). The message must include:
|
||||
|
||||
- Unique "public" key (`EK`) that Bob must use to encrypt messages.
|
||||
|
||||
- SMP server hostname and information to open secure encrypted transport connection (see [Appendix A](#appendix-a)).
|
||||
|
||||
- Sender queue ID `SID` for Bob to use.
|
||||
|
||||
3. Bob, having received the out-of-band message from Alice, connects to the queue:
|
||||
|
||||
1. Generates a new random public/private key pair (sender key - `SK`) that he did not use before for him to sign messages sent to Alice's server.
|
||||
|
||||
2. Prepares the confirmation message for Alice to secure the queue. This message includes:
|
||||
|
||||
- Previously generated "public" key `SK` that will be used by Alice's server to authenticate Bob's messages, once the queue is secured.
|
||||
|
||||
- Optionally, any additional information (application specific, e.g. Bob's profile name and details).
|
||||
|
||||
3. Encrypts the confirmation body with the "public" key `EK` (that Alice provided via the out-of-band message).
|
||||
|
||||
4. Sends the encrypted message to the server with queue ID `SID` (see `send` in [Send message](#send-message)). This initial message to the queue must not be signed - signed messages will be rejected until Alice secures the queue (below).
|
||||
|
||||
4. Alice receives Bob's message from the server using recipient queue ID `RID` (possibly, via the same transport connection she already has opened - see `message` in [Deliver queue message](#deliver-queue-message)):
|
||||
|
||||
1. She decrypts received message with "private" key `EK`.
|
||||
|
||||
2. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol.
|
||||
|
||||
5. Alice secures the queue `RID` with `"KEY"` command so only Bob can send messages to it (see [Secure queue command](#secure-queue-command)):
|
||||
|
||||
1. She sends the `KEY` command with `RID` signed with "private" key `RK` to update the queue to only accept requests signed by "private" key `SK` provided by Bob.
|
||||
|
||||
2. From this moment the server will accept only signed commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has).
|
||||
|
||||
3. Once queue is secured, Alice deletes `SID` and `SK` - even if Alice's client is compromised in the future, the attacker would not be able to send messages pretending to be Bob.
|
||||
|
||||
6. The simplex queue `RID` is now ready to be used.
|
||||
|
||||
This flow is shown on the sequence diagram below.
|
||||
|
||||
**Creating simplex queue from Bob to Alice:**
|
||||
|
||||

|
||||
|
||||
Bob now can securely send messages to Alice:
|
||||
|
||||
1. Bob sends the message:
|
||||
|
||||
1. He encrypts the message to Alice with "public" key `EK` (provided by Alice, only known to Bob, used only for one simplex queue).
|
||||
|
||||
2. He signs `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue).
|
||||
|
||||
3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier provided to the server).
|
||||
|
||||
2. Alice receives the message(s):
|
||||
|
||||
1. She signs `"SUB"` command to the server to subscribe to the queue `RID` with the "private" key `RK` (see `subscribe` in [Subscribe to queue](#subscribe-to-queue)).
|
||||
|
||||
2. The server, having authenticated Alice's command with the "public" key `RK` that she provided, delivers Bob's message(s) (see `message` in [Deliver queue message](#deliver-queue-message)).
|
||||
|
||||
3. She decrypts Bob's message(s) with the "private" key `EK` (that only she has).
|
||||
|
||||
4. She acknowledges the message reception to the server with `"ACK"` so that the server can delete the message and deliver the next messages.
|
||||
|
||||
This flow is show on sequence diagram below.
|
||||
|
||||
**Sending messages from Bob to Alice via simplex queue:**
|
||||
|
||||

|
||||
|
||||
**Simplex queue operation:**
|
||||
|
||||

|
||||
|
||||
Sequence diagram does not show E2E encryption - server knows nothing about encryption between the sender and the receiver.
|
||||
|
||||
A higher level protocol application protocol should define the semantics that allow to use two simplex queues (or two sets of queues for redundancy) for the bi-directional or any other communication scenarios.
|
||||
|
||||
The SMP is intentionally unidirectional - it provides no answer to how Bob will know that the transmission succeeded, and whether Alice received any messages. There may be a scenario when Alice wants to securely receive the messages from Bob, but she does not want Bob to have any proof that she received any messages - this low-level protocol can be used in this scenario, as all Bob knows as a fact is that he was able to send one unsigned message to the server that Alice provided, and now he can only send messages signed with the key `SK` that he sent to the server - it does not prove that any message was received by Alice.
|
||||
|
||||
For practical purposes of bi-directional conversation, now that Bob can securely send encrypted messages to Alice, Bob can create the second simplex queue that will allow Alice to send messages to Bob in the same way, sending the second queue details via the first queue. If both Alice and Bob have their respective unique "public" keys (Alice's and Bob's `EK`s of two separate queues), or pass additional keys to sign the messages, the conversation can be both encrypted and signed.
|
||||
|
||||
The established queues can also be used to change the encryption keys providing [forward secrecy][5], or to negotiate using other SMP queue(s).
|
||||
|
||||
This protocol also can be used for off-the-record messaging, as Alice and Bob can use multiple queues between them and only information they pass to each other allows proving their identity, so if they want to share anything off-the-record they can initiate a new queue without linking it to any other information they exchanged. As a result, this protocol provides better anonymity and better protection from [MITM][1] than [OTR][6] protocol.
|
||||
|
||||
## SMP qualities and features
|
||||
|
||||
Simplex Messaging Protocol:
|
||||
|
||||
- Defines only message-passing protocol:
|
||||
|
||||
- Transport agnostic - the protocol does not define how clients connect to the servers. It can be implemented over any ordered data stream channel: TCP connection, HTTP with long polling, websockets, etc.
|
||||
|
||||
- Not semantic - the protocol does not assign any meaning to queues and messages. While on the application level the queues and messages can have different meaning (e.g., for messages: text or image chat message, message acknowledgement, participant profile information, status updates, changing "public" key to encrypt messages, changing servers, etc.), on SMP protocol level all the messages are binary and their meaning can only be interpreted by client applications and not by the servers - this interpretation is out of scope of this protocol.
|
||||
|
||||
- Client-server architecture:
|
||||
|
||||
- Multiple servers, that can be deployed by the system users, can be used to send and retrieve messages.
|
||||
|
||||
- Servers do not communicate with each other and do not "know" about other servers.
|
||||
|
||||
- Clients only communicate with servers (excluding the initial out-of-band message), so the message passing is asynchronous.
|
||||
|
||||
- For each queue, the message recipient defines the server through which the sender should send messages.
|
||||
|
||||
- While multiple servers and multiple queues can be used to pass each message, it is in scope of application level protocol(s), and out of scope of this protocol.
|
||||
|
||||
- Servers store messages only until they are retrieved by the recipients, and in any case, for a limited time.
|
||||
|
||||
- Servers are required to NOT store any message history or delivery log, but even if the server is compromised, it does not allow to decrypt the messages or to determine the list of queues established by any participant - this information is only stored on client devices.
|
||||
|
||||
- The only element provided by SMP servers is simplex queues:
|
||||
|
||||
- Each queue is created and managed by the queue recipient.
|
||||
|
||||
- Asymmetric encryption is used to sign and verify the requests to send and receive the messages.
|
||||
|
||||
- One unique "public" key is used by the servers to authenticate requests to send the messages into the queue, and another unique "public" key - to retrieve the messages from the queue. "Unique" here means that each "public" key is used only for one queue and is not used for any other context - effectively, this key is not public and does not represent any participant identity.
|
||||
|
||||
- Both "public" keys are provided to the server by the queue recipient when the queue is created.
|
||||
|
||||
- The "public" keys known to the server and used to authenticate commands from the participants are unrelated to the keys used to encrypt and decrypt the messages - the latter keys are also unique per each queue but they are only known to participants, not to the servers.
|
||||
|
||||
- Messaging graph can be asymmetric: Bob's ability to send messages to Alice does not automatically lead to the Alice's ability to send messages to Bob.
|
||||
|
||||
## Cryptographic algorithms
|
||||
|
||||
Simplex messaging clients need to cryptographically sign commands for the following operations:
|
||||
|
||||
- With the recipient's key `RK` (server to verify):
|
||||
- create the queue (`NEW`)
|
||||
- subscribe to queue (`SUB`)
|
||||
- secure the queue (`KEY`)
|
||||
- acknowledge received messages (`ACK`)
|
||||
- suspend the queue (`OFF`)
|
||||
- delete the queue (`DEL`)
|
||||
- With the sender's key `SK` (server to verify):
|
||||
- send messages (`SEND`)
|
||||
|
||||
To sign and verify commands, clients and servers MUST use RSA-PSS algorithm defined in [RFC3447][2].
|
||||
|
||||
To optionally sign and verify messages, clients SHOULD use RSA-PSS algorithm.
|
||||
|
||||
To encrypt and decrypt messages, clients and servers SHOULD use RSA-OAEP algorithm defined in [RFC3447][2].
|
||||
|
||||
The reasons to use these algorithms:
|
||||
|
||||
- They are supported by WebCrypto API.
|
||||
- They are more widely supported than ECC algorithms.
|
||||
- They are newer versions than RSA-PKCS1-v1_5 encryption and signature schemes.
|
||||
|
||||
Future versions of the protocol may allow different cryptographic algorithms.
|
||||
|
||||
## Simplex queue IDs
|
||||
|
||||
Simplex messaging servers MUST generate 2 different IDs for each new queue - for the recipient (that created the queue) and for the sender. It is REQUIRED that:
|
||||
|
||||
- These IDs are different and unique within the server.
|
||||
- Based on random bytes generated with cryptographically strong pseudo-random number generator.
|
||||
|
||||
## Server security requirements
|
||||
|
||||
Simplex messaging server implementations MUST NOT create, store or send to any other servers:
|
||||
|
||||
- Logs of the client commands and transport connections in the production environment.
|
||||
|
||||
- History of deleted queues, retrieved or acknowledged messages (deleted queues MAY be stored temporarily as part of the queue persistence implementation).
|
||||
|
||||
- Snapshots of the database they use to store queues and messages (instead simplex messaging clients must manage redundancy by using more than one simplex messaging server). In-memory persistence is recommended.
|
||||
|
||||
- Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using simplex messaging servers.
|
||||
|
||||
## SMP commands
|
||||
|
||||
Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a].
|
||||
|
||||
Each transmission between the client and the server must have this format/syntax (after the decryption):
|
||||
|
||||
```abnf
|
||||
transmission = [signature] SP signed SP pad ; pad to the fixed block size
|
||||
signed = [corrId] SP [queueId] SP cmd
|
||||
cmd = ping / recipientCmd / send / serverMsg
|
||||
recipientCmd = create / subscribe / secure / acknowledge / suspend / delete
|
||||
serverMsg = pong / queueIds / message / unsubscribed / ok / error
|
||||
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
|
||||
queueId = encoded ; empty queue ID is used with "create" command
|
||||
signature = encoded
|
||||
; empty signature can be used with "create", "send" and "ping" commands and server messages
|
||||
encoded = <base64 encoded binary>
|
||||
```
|
||||
|
||||
`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9]
|
||||
|
||||
The syntax of specific commands and responses is defined below.
|
||||
|
||||
### Correlating responses with commands
|
||||
|
||||
The server should send `queueIds`, `error` and `ok` responses in the same order within each queue ID as the commands received in the transport connection, so that they can be correlated by the clients. To simplify correlation of commands and responses, the server should use the same `corrId` in the response as in the command sent by the client.
|
||||
|
||||
If the transport connection is closed before some responses are sent, these responses should be discarded.
|
||||
|
||||
### Command authentication
|
||||
|
||||
SMP servers must authenticate all transmissions (excluding `ping` and `send` commands) by verifying the provided signatures. Command signature should be generated by applying RSA-PSS algorithm to the `signed` block of the transmission using the key associated with the queue ID (sender's or recipient's, depending on which queue ID is used).
|
||||
|
||||
### Keep-alive command
|
||||
|
||||
To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `pong` response. This command should be sent unsigned and without queue ID.
|
||||
|
||||
```abnf
|
||||
ping = %s"PING"
|
||||
pong = %s"PONG"
|
||||
```
|
||||
|
||||
### Recipient commands
|
||||
|
||||
Sending any of the commands in this section (other than `create`, that is sent without queue ID) is only allowed with recipient's ID (`RID`). If sender's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)).
|
||||
|
||||
#### Create queue command
|
||||
|
||||
This command is sent by the recipient to the SMP server to create a new queue. The syntax is:
|
||||
|
||||
```abnf
|
||||
create = %s"NEW" SP recipientKey
|
||||
recipientKey = %s"rsa:" x509encoded ; the recipient's RSA public key for this queue
|
||||
x509encoded = <base64 X509 key encoding>
|
||||
```
|
||||
|
||||
If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs:
|
||||
|
||||
```abnf
|
||||
queueIds = %s"IDS" SP recipientId SP senderId
|
||||
recipientId = encoded
|
||||
senderId = encoded
|
||||
```
|
||||
|
||||
This response should be sent with empty queue ID (the second part of the transmission).
|
||||
|
||||
Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened.
|
||||
|
||||
NEW `transmission` must be signed using the `recipientKey` that was passed in the transmission.
|
||||
|
||||
#### Subscribe to queue
|
||||
|
||||
When the simplex queue was not created in the current transport connection, the recipient must use this command to start receiving messages from it:
|
||||
|
||||
```abnf
|
||||
subscribe = %s"SUB"
|
||||
```
|
||||
|
||||
If subscription is successful the server must respond with the first available message or with `ok` response if no messages are available. The recipient will continue receiving the messages from this queue until the transport connection is closed or until another transport connection subscribes to the same simplex queue - in this case the first subscription should be cancelled and [subscription END notification](#subscription-end-notification) delivered.
|
||||
|
||||
The first message will be delivered either immediately or as soon as it is available; to receive the following message the recipient must acknowledge the reception of the message (see [Acknowledge message delivery](#acknowledge-message-delivery)).
|
||||
|
||||
#### Secure queue command
|
||||
|
||||
This command is sent by the recipient to the server to add sender's key to the queue:
|
||||
|
||||
```
|
||||
secure = %s"KEY" SP senderKey
|
||||
senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue
|
||||
```
|
||||
|
||||
`senderKey` is received from the sender as part of the first message - see [Send Message Command](#send-message-command).
|
||||
|
||||
Once the queue is secured only signed messages can be sent to it.
|
||||
|
||||
#### Acknowledge message delivery
|
||||
|
||||
The recipient should send the acknowledgement of message delivery once the message was stored in the client, to notify the server that the message should be deleted:
|
||||
|
||||
```abnf
|
||||
acknowledge = %s"ACK"
|
||||
```
|
||||
|
||||
Even if acknowledgement is not sent by the recipient, the server should limit the time of message storage, whether it was delivered to the recipient or not.
|
||||
|
||||
Having received the acknowledgement, SMP server should immediately delete the message and then send the next available message or respond with `ok` if there are no more messages available in this simplex queue.
|
||||
|
||||
#### Suspend queue
|
||||
|
||||
The recipient can suspend a queue prior to deleting it to make sure that no messages are lost:
|
||||
|
||||
```abnf
|
||||
suspend = %s"OFF"
|
||||
```
|
||||
|
||||
The server must respond with `"ERR AUTH"` to any messages sent after the queue was suspended (see [Error responses](#error-responses)).
|
||||
|
||||
The server must respond `ok` to this command if it was successful.
|
||||
|
||||
This command can be sent multiple times (in case transport connection was interrupted and the response was not delivered), the server should still respond `ok` even if the queue is already suspended.
|
||||
|
||||
There is no command to resume the queue. Servers must delete suspended queues that were not deleted after some period of time.
|
||||
|
||||
#### Delete queue
|
||||
|
||||
The recipient can delete the queue, whether it was suspended or not.
|
||||
|
||||
All undelivered messages must be deleted as soon as this command is received, before the response is sent.
|
||||
|
||||
```abnf
|
||||
delete = %s"DEL"
|
||||
```
|
||||
|
||||
### Sender commands
|
||||
|
||||
Currently SMP defines only one command that can be used by senders - `send` message. This command must be used with sender's ID, if recipient's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)).
|
||||
|
||||
#### Send message
|
||||
|
||||
This command is sent to the server by the sender both to confirm the queue after the sender received out-of-band message from the recipient and to send messages after the queue is secured:
|
||||
|
||||
```abnf
|
||||
send = %s"SEND" SP size SP msgBody SP
|
||||
; the last SP is in addition to SP in the transmission
|
||||
size = 1*DIGIT ; size in bytes
|
||||
msgBody = *OCTET ; any binary content of specified size
|
||||
```
|
||||
|
||||
The first message is sent to confirm the queue - it should contain sender's server key (see decrypted message syntax below) - this first message must be sent without signature.
|
||||
|
||||
Once the queue is secured (see [Secure queue command](#secure-queue-command)), the following send commands must be sent with the signature.
|
||||
|
||||
The server must respond with `"ERR AUTH"` response in the following cases:
|
||||
|
||||
- the queue does not exist or is suspended
|
||||
- the queue is secured but the transmission does NOT have a signature
|
||||
- the queue is NOT secured but the transmission has a signature
|
||||
|
||||
Until the queue is secured, the server should accept any number of unsigned messages - it both enables the legitimate sender to resend the confirmation in case of failure and also allows the simplex messaging client to ignore any confirmation messages that may be sent by the attackers (assuming they could have intercepted the queue ID in the server response, but do not have a correct encryption key passed to sender in out-of-band message).
|
||||
|
||||
The body should be encrypted with the recipient's "public" key (`EK`); once decrypted it must have this format:
|
||||
|
||||
```abnf
|
||||
decryptedBody = [clientHeader] CRLF clientBody CRLF
|
||||
clientHeader = senderKeyMsg
|
||||
senderKeyMsg = %s"KEY" SP senderKey
|
||||
senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue
|
||||
clientBody = *OCTET
|
||||
```
|
||||
|
||||
`clientHeader` in the initial unsigned message is used to transmit sender's server key and can be used in the future revisions of SMP protocol for other purposes.
|
||||
|
||||
### Server messages
|
||||
|
||||
#### Queue IDs response
|
||||
|
||||
Server must respond with this message when the new queue is created.
|
||||
|
||||
See its syntax in [Create queue command](#create-queue-command)
|
||||
|
||||
#### Deliver queue message
|
||||
|
||||
The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is:
|
||||
|
||||
```abnf
|
||||
message = %s"MSG" SP msgId SP timestamp SP size SP msgBody SP
|
||||
msgId = encoded
|
||||
timestamp = <date-time defined in RFC3339>
|
||||
```
|
||||
|
||||
`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement).
|
||||
|
||||
`timestamp` - the UTC time when the server received the message from the sender, must be in date-time format defined by [RFC 3339][10]
|
||||
|
||||
`binaryMsg` - see syntax in [Send message](#send-message)
|
||||
|
||||
#### Subscription END notification
|
||||
|
||||
When another transport connection is subscribed to the same simplex queue, the server should unsubscribe and to send the notification to the previously subscribed transport connection:
|
||||
|
||||
```abnf
|
||||
unsubscribed = %s"END"
|
||||
```
|
||||
|
||||
No further messages should be delivered to unsubscribed transport connection.
|
||||
|
||||
#### Error responses
|
||||
|
||||
- incorrect block format, encoding or signature size (`BLOCK`).
|
||||
- command errors (`CMD`):
|
||||
- error parsing command (`SYNTAX`)
|
||||
- prohibited command (`PROHIBITED`) - any server response sent from client or `ACK` sent without active subscription or without message delivery.
|
||||
- incorrect RSA key size in `NEW` or `KEY` commands - only 1024, 2048 and 4096-bit keys are allowed (`KEY_SIZE`).
|
||||
- transmission has no required signature or queue ID (`NO_AUTH`)
|
||||
- transmission has unexpected credentials (`HAS_AUTH`)
|
||||
- transmission has no required queue ID (`NO_QUEUE`)
|
||||
- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message command](#send-message-command)).
|
||||
- incorrect message body size (`SIZE`).
|
||||
- internal server error (`INTERNAL`).
|
||||
|
||||
The syntax for error responses:
|
||||
|
||||
```abnf
|
||||
error = %s"ERR" SP errorType
|
||||
errorType = %s"BLOCK" / %s"CMD" SP cmdError / %s"AUTH" / %s"SIZE" /%s"INTERNAL"
|
||||
cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"KEY_SIZE" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_QUEUE"
|
||||
```
|
||||
|
||||
Server implementations must aim to respond within the same time for each command in all cases when `"ERR AUTH"` response is required to prevent timing attacks (e.g., the server should perform signature verification even when the queue does not exist on the server or the signature of different size is sent, using any RSA key with the same size as the signature size).
|
||||
|
||||
### OK response
|
||||
|
||||
When the command is successfully executed by the server, it should respond with OK response:
|
||||
|
||||
```abnf
|
||||
ok = %s"OK"
|
||||
```
|
||||
|
||||
## Appendices
|
||||
|
||||
### Appendix A.
|
||||
|
||||
**SMP transport protocol.**
|
||||
|
||||
Both the recipient and the sender can use TCP or some other, possibly higher level, transport protocol to communicate with the server. The default TCP port for SMP server is 5223.
|
||||
|
||||
By default, the client and server should use the protocol presented below, that does not depend on a centralized certificate authority.
|
||||
|
||||
Transport is encrypted with [AEAD-GCM][12] protocol with two random symmetric AES 256-bit keys and two random base IVs that will be agreed during the handshake. Both client and the server should maintain two 32-bit word counters, one for the sent and one for the received messages. The IV for each message should be computed by xor-ing the sequential message counter, starting from 0, with the first 32 bits of agreed base IV (the number is encoded in network byte order).
|
||||
|
||||
To establish the session keys and base IVs, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) in advance in order to be able to validate the server public key during transport connection handshake.
|
||||
|
||||
The handshake sequence is the following:
|
||||
|
||||
1. Once the connection is established, the server sends the `server_header` followed by its public RSA key encoded in X509 binary (not base-64 encoded) format to the client.
|
||||
2. The client compares the SHA256 hash of the received key with the hash it already has (e.g. received as part of connection invitation or as SMP server configuration). If the hash does not match, the client must terminate the connection.
|
||||
3. If the hash is the same, the client should generate two random symmetric 256-bit AES keys and two base IVs that will be used as session keys/IVs by the client and the server.
|
||||
4. The client then should create the `client_handshake` block and send it to the server, encrypted using [RSA-OAEP][2] scheme with the server public key: `rsa-encrypt(client_handshake)`. `snd_aes_key` and `snd_base_iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv_aes_key` and `rcv_base_iv` will be used by the client to decrypt **received** messages and by the server to encrypt them. `client_handshake` also contains `block_size` and reserved `protocol` blocks (see syntax).
|
||||
5. The server should decrypt the received AES keys and base IVs with its private RSA key.
|
||||
6. In case of successful decryption, the server should send encrypted welcome block (`encrypted_welcome_block`) that contains SMP protocol version supported by the server.
|
||||
|
||||
All the subsequent data, both from the client and from the server, should be sent padded to the fixed agreed block size, encrypted with symmetric AES keys and base IVs (incremented by counters on both sides), that were sent by the client during the handshake. If the application needs to transmit a larger message, it should be broken down into fragments.
|
||||
|
||||
Handshake blocks sent by the client and the server have this syntax:
|
||||
|
||||
```abnf
|
||||
server_header = block_size protocol key_size
|
||||
block_size = 4*4(OCTET) ; 4-byte block size sent by the server
|
||||
protocol = 2*2(%x00) ; 0, reserved
|
||||
key_size = 2*2(OCTET) ; the size of the encoded key in bytes (binary encoded in X509 standard)
|
||||
|
||||
client_handshake = client_block_size protocol snd_aes_key snd_base_iv rcv_aes_key rcv_base_iv
|
||||
client_block_size = 4*4(OCTET) ; 4-byte block size sent by the client,
|
||||
; to confirm or override the block size sent by the server
|
||||
client_protocol = 2*2(%x00) ; 0, reserved
|
||||
snd_aes_key = 32*32(OCTET)
|
||||
snd_base_iv = 16*16(OCTET)
|
||||
rcv_aes_key = 32*32(OCTET)
|
||||
rcv_base_iv = 16*16(OCTET)
|
||||
|
||||
transport_block = aes_body_auth_tag aes_encrypted_body
|
||||
; size is sent by server during handshake, usually 4096 bytes
|
||||
aes_body_auth_tag = 16*16(OCTET)
|
||||
aes_encrypted_body = 1*OCTET
|
||||
|
||||
encrypted_welcome_block = transport_block
|
||||
welcome_block = smp_version SP pad ; decrypt(encrypted_welcome_block)
|
||||
smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format
|
||||
; for example: v123.456.789-alpha.7
|
||||
pad = 1*OCTET
|
||||
```
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
||||
[2]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
[3]: https://en.wikipedia.org/wiki/QR_code
|
||||
[4]: https://en.wikipedia.org/wiki/Unidirectional_network
|
||||
[5]: https://en.wikipedia.org/wiki/Forward_secrecy
|
||||
[6]: https://en.wikipedia.org/wiki/Off-the-Record_Messaging
|
||||
[8]: https://tools.ietf.org/html/rfc5234
|
||||
[8a]: https://tools.ietf.org/html/rfc7405
|
||||
[9]: https://tools.ietf.org/html/rfc4648#section-4
|
||||
[10]: https://tools.ietf.org/html/rfc3339
|
||||
[11]: https://tools.ietf.org/html/rfc5280
|
||||
[12]: https://tools.ietf.org/html/rfc7714
|
||||
@@ -0,0 +1,70 @@
|
||||
# SMP agent logging
|
||||
|
||||
## Problem and proposed solution.
|
||||
|
||||
SMP agent performs multiple actions in response to the client commands and to the messages received from other SMP agents (wrapped in SMP protocol messages).
|
||||
|
||||
Customary approach for the network protocol clients is to have a debug/verbose mode that enables logging of all sent and received messages and any other actions that the client performs.
|
||||
|
||||
This document proposes a logging format for SMP agent that would be enabled if the agent is run with `--verbose` / `-v` command line option.
|
||||
|
||||
We can also consider logging the database operations that change the data.
|
||||
|
||||
## Types of actions and the associated log line format.
|
||||
|
||||
### Client connected / disconnected
|
||||
|
||||
```
|
||||
client n connected to Agent
|
||||
client n disconnected from Agent
|
||||
```
|
||||
|
||||
where `n` is a sequential number of a connected agent client, starting from 1 (over the agent run-time).
|
||||
|
||||
### Server connected / disconnected
|
||||
|
||||
```
|
||||
Agent connected to host:port
|
||||
Agent disconnected from host:port
|
||||
```
|
||||
|
||||
### Received command from the client
|
||||
|
||||
```
|
||||
n --> A : corrId connAlias parsed_command // raw_command
|
||||
```
|
||||
|
||||
`raw_command` is added only in case of parsing failure.
|
||||
|
||||
### Sent command to SMP server
|
||||
|
||||
```
|
||||
A --> host:port : corrId queueId parsed_command
|
||||
```
|
||||
|
||||
### Received response / message from the SMP server
|
||||
|
||||
```
|
||||
A <-- host:port : corrId queueId parsed_command // raw_command
|
||||
```
|
||||
|
||||
In case the response is a message or notification, corrId should be replaced with `_`
|
||||
|
||||
### Interpreted ("unwrapped") SMP message as agent message
|
||||
|
||||
```
|
||||
Agent msg : connAlias parsed_message // raw_message
|
||||
```
|
||||
|
||||
### Sent response / message to the client
|
||||
|
||||
```
|
||||
n <-- A : corrId connAlias parsed_command // raw_command
|
||||
```
|
||||
|
||||
### Database changes
|
||||
|
||||
```
|
||||
DB : insert/delete/update table key
|
||||
DB : insert/delete/update table key
|
||||
```
|
||||
@@ -0,0 +1,192 @@
|
||||
# SMP agent: cryptography
|
||||
|
||||
3 main directions of work to enable basic level of security for communication via SMP agents and servers at the current stage of the project:
|
||||
|
||||
- Transport encryption - enable encryption for TCP.
|
||||
|
||||
- Initial handshake using asymmetric key pairs, starting with out-of-band message.
|
||||
|
||||
- E2E encryption of messages between SMP agents relayed over SMP servers.
|
||||
|
||||
- Authentication of agent commands with SMP servers.
|
||||
|
||||
For initial implementation I propose approach to be as simple as possible as long as it meets our security requirements. So no pluggable encryption mechanisms, no configuration, no integration with [Noise Protocol Framework](https://noiseprotocol.org), only the most necessary Crypto schemes pre-decided per area of application.
|
||||
|
||||
## Transport encryption
|
||||
|
||||
One of the consideration is to use [noise protocol framework](https://noiseprotocol.org/noise.html), this section describes ad hoc protocol though.
|
||||
|
||||
During TCP session both client and server should use symmetric AES 256 bit encryption using two session keys and two base IVs that will be agreed during the handshake. Both client and the server should maintain two 32-bit word counters, one for sent and one for the received messages. The IV for each message should be computed by xor-ing the sequential message counter, starting from 0, with the first 32 bits of agreed base IV. TODO - explain it in a more formal way, also document how 32-bit word is encoded - with the most or least significant byte first (currently encodeWord32 from Network.Transport.Internal is used)
|
||||
|
||||
To establish the session keys and base IVs, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) in advance in order to be able to establish connection.
|
||||
|
||||
The handshake sequence is the following:
|
||||
|
||||
1. Once the connection is established, the server sends server_header and its public RSA key encoded in X509 binary format to the client.
|
||||
2. The client compares the hash of the received key with the hash it already has (e.g. received as part of connection invitation or server in NEW command). If the hash does not match, the client must terminate the connection. TODO as the hash is optional in server syntax at the moment, hash comparison will be optional as well. Probably it should become required.
|
||||
3. If the hash is the same, the client should generate random symmetric AES keys and base IVs that will be used as session keys/IVs by the client and the server.
|
||||
4. The client then should construct client_handshake block and send it to the server encrypted with the server public key: `rsa-encrypt(client_handshake)`. `snd_aes_key` and `snd_base_iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv_aes_key` and `rcv_base_iv` will be used by the client to decrypt **received** messages and by the server to encrypt them.
|
||||
5. The server should decrypt the received keys and base IVs with its private key.
|
||||
6. In case of successful decryption, the server should send encrypted welcome block (encrypted_welcome_block) that contains SMP protocol version.
|
||||
|
||||
All the subsequent data both from the client and from the server should be sent encrypted using symmetric AES keys and base IVs (incremented by counters on both sides) sent by the client during the handshake.
|
||||
|
||||
Each transport block sent by the client and the server has this syntax:
|
||||
|
||||
```abnf
|
||||
server_header = block_size protocol key_size
|
||||
block_size = 4*4(OCTET) ; 4-byte block size sent by the server, currently the client rejects if > 65536 bytes
|
||||
protocol = 2*2(OCTET) ; currently it is 0, that means binary RSA key
|
||||
key_size = 2*2(OCTET) ; the encoded key size in bytes (binary encoded in X509 standard)
|
||||
|
||||
client_handshake = client_block_size client_protocol snd_aes_key snd_base_iv rcv_aes_key rcv_base_iv
|
||||
client_block_size = 4*4(OCTET) ; 4-byte block size sent by the client, currently it is ignored by the server - reserved
|
||||
client_protocol = 2*2(OCTET) ; currently it is 0 - reserved
|
||||
snd_aes_key = 32*32(OCTET)
|
||||
snd_base_iv = 16*16(OCTET)
|
||||
rcv_aes_key = 32*32(OCTET)
|
||||
rcv_base_iv = 16*16(OCTET)
|
||||
|
||||
transport_block = aes_body_auth_tag aes_encrypted_body
|
||||
; size is sent by server during handshake, usually 8192 bytes
|
||||
aes_encrypted_body = 1*OCTET
|
||||
aes_body_auth_tag = 16*16(OCTET)
|
||||
|
||||
encrypted_welcome_block = transport_block
|
||||
welcome_block = smp_version SP pad ; decrypt(encrypted_welcome_block)
|
||||
smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format
|
||||
; for example: v123.456.789-alpha.7
|
||||
pad = 1*OCTET
|
||||
```
|
||||
|
||||
## Possible future improvements/changes
|
||||
|
||||
- server id (256 bits), so that only the users that have it can connect to the server. This ID will have to be passed to the server during the handshake
|
||||
- block size agreed during handshake
|
||||
- transport encryption protocol agreed during handshake
|
||||
- welcome block containing SMP mode (smp_mode)
|
||||
|
||||
```abnf
|
||||
smp_mode = smp_public / smp_authenticated
|
||||
smp_public = %s"pub" ; public (default) - no auth to create and manage queues
|
||||
smp_authenticated = %s"auth" ; server authentication with AUTH command (TBD) is required to create and manage queues
|
||||
```
|
||||
|
||||
## Initial handshake
|
||||
### Why handshake has to be with asymmetric keys
|
||||
|
||||
The following only makes sense under the assumption that public keys are not really used as "public", but are created as random and treated as secret instead.
|
||||
|
||||
E controls servers & listens out-of-band.
|
||||
Keys are generated on the clients (A & B), queues are generated on servers.
|
||||
1. A generates Apub, Aprv, BAq
|
||||
2. A --oob-> B: Apub, BAq; E listens this, however she can't substitute this (passive attack on out-of-band, active on servers)
|
||||
3. E generates EpubA, EprvA, AEq
|
||||
4. E --BAq-> A: EpubA, AEq - encrypted with Apub;
|
||||
Alice thinks this message is from Bob
|
||||
5. B generates Bpub, Bprv, ABq
|
||||
6. B --BAq-> ~~A~~E: Bpub, ABq - encrypted with Apub;
|
||||
Eve controls servers so this doesn't get sent to Alice, instead it is received by Eve
|
||||
7. Eve has to send to Bob via ABq encrypting with his Bpub. By controlling servers E could know ABq so she wouldn't have to decrypt it - she knows where to send. Even so she can't decrypt Bpub w/t Aprv. The problem [for Eve] is that Bpub and Aprv are generated on the clients, which she doesn't control.
|
||||
|
||||
If keys were symmetric Eve could simply decrypt Bob's key with the key Alice sent out-of-band.
|
||||
|
||||
### Where MITM attempt fails
|
||||
|
||||
In asymmetric setup the following happens:
|
||||
|
||||
- In Bob to Alice direction Eve intercepts messages from Bob and re-encrypts them using Apub.
|
||||
|
||||
- In Alice to Bob direction Alice encrypts messages with EpubA and Eve can decrypt them with EprvA. Eve can't, however, re-encrypt them for Bob because she doesn't know Bpub. She also couldn't substitute it because it is out of her control. Alice wouldn't [technically] notice the MITM attempt, but Bob will not be receiving decryptable messages, and he would have to tell Alice out-of-band.
|
||||
|
||||
> **To be discussed:** Implementation-wise Bob's connection probably should be shut down if he receives a message he fails to decrypt, or after some timeout if he doesn't receive messages.
|
||||
|
||||
### Handshake implementation
|
||||
|
||||
TODO
|
||||
|
||||
## E2E encryption
|
||||
|
||||
For E2E encryption of messages between SMP agents we should go with some robust [Authenticated Encyption](https://en.wikipedia.org/wiki/Authenticated_encryption) scheme following [Encrypt-then-MAC](https://en.wikipedia.org/wiki/Authenticated_encryption#Encrypt-then-MAC_(EtM)) approach.
|
||||
|
||||
Since we have a shared secret Apub, Bpub (if Apub is compromised connection should be shut down on Bob's side, see [above](#-Where-MITM-attempt-fails)) there is no point in using digital signatures over MACs for message authentication other than non-repudiation. Besides [digital signatures generally being less performant than MACs](https://crypto.stackexchange.com/a/37657), the non-repudiation quality I believe may in fact be more undesirable than not for many possible applications. If some applications require non-repudiation it can be implemented later on with digital signatures on application level. See a good answer on differences of MAC and digital signature qualities [here](https://crypto.stackexchange.com/a/5647).
|
||||
|
||||
Symmetric keys are generated per message and encrypted with receiver's public key (encryption key).
|
||||
|
||||
The syntax of each encrypted message body is the following:
|
||||
|
||||
```abnf
|
||||
encrypted_message_body = rsa_signature encrypted_body
|
||||
encrypted_body = rsa_encrypted_header aes_encrypted_body
|
||||
rsa_signature = 256*256(OCTET) ; sign(encrypted_body) - assuming 2048 bit key size
|
||||
rsa_encrypted_header = 256*256(OCTET) ; encrypt(header) - assuming 2048 bit key size
|
||||
aes_encrypted_body = 1*OCTET ; encrypt(body)
|
||||
|
||||
header = aes_key aes_iv auth_tag payload_size
|
||||
aes_key = 32*32(OCTET)
|
||||
aes_iv = 16*16(OCTET)
|
||||
auth_tag = 16*16(OCTET)
|
||||
payload_size = 4*4(OCTET)
|
||||
|
||||
body = payload pad
|
||||
```
|
||||
|
||||
Future considerations:
|
||||
- Generation of symmetric keys per session and session rotation;
|
||||
|
||||
## E2E implementation
|
||||
|
||||
### Message encryption
|
||||
|
||||
For each message, the agent performs the following actions to encrypt it:
|
||||
|
||||
1. Generates random AES256 key with [getRandomBytes]() - see [tutorial](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Tutorial.html)
|
||||
2. Pad the message using some padding scheme TBC - we could consider [PADMÉ](https://petsymposium.org/2019/files/papers/issue4/popets-2019-0056.pdf), but there is no existing implementation. This particular scheme is protected against both understanding the nature of the traffic and has low overhead (< 12%). There are various other padding schemes described [here](https://en.wikipedia.org/wiki/Padding_(cryptography)) - the proposal is to postpone this decision and not to include any padding in the chat prototype to be released.
|
||||
3. Encrypt the message using this AES256 key with [AES-GCM-SIV](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Cipher-AESGCMSIV.html) (AEAD scheme) using the function `encrypt` with a random nonce.
|
||||
4. Encrypt the AES256 key with public encryption key for the queue using RSA-OAEP [encrypt](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-OAEP.html#v:encrypt) function parameterized with [SHA256](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Hash-Algorithms.html#t:SHA256) algorithm.
|
||||
5. The message to send is concatenation of:
|
||||
1. encrypted AES256 key,
|
||||
2. AuthTag from encryption in step 3,
|
||||
3. encrypted message (in this order)
|
||||
|
||||
As there is no additional data that is sent e2e in clear text, we potentially need some simpler algorithm, possibly the one implemented in the tutorial, that implements authenticated encryption without additional (unencrypted) data.
|
||||
|
||||
Or we could use this algorithm to allow some data that is sent e2e in clear text later.
|
||||
|
||||
### Message decryption
|
||||
|
||||
Agent decrypt the message following the same steps in the opposite order
|
||||
|
||||
1. Split encrypted message to AES256 key, AuthTag and encrypted message - tag and key have fixed size (?).
|
||||
2. Decrypt AES256 key using the decryption key (the private key that recipient has).
|
||||
3. Decrypt the message using AES256 key.
|
||||
4. Remove the padding - scheme TBC, not in the initial implementation.
|
||||
|
||||
## Authentication with SMP server
|
||||
|
||||
Each command sent to the server should include digital signature in the first part of the SMP transmission.
|
||||
|
||||
To compute the signature, the agent will use [signSafer](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-PSS.html#v:signSafer) function parameterized with [SHA256](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Hash-Algorithms.html#t:SHA256) algorithm with the "signed" part of the transmission.
|
||||
|
||||
To verify the signature, the server will use [verify](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-PSS.html#v:verify) function with the same hash algorithm.
|
||||
|
||||
The above implies that the way the transmissions are processed should change.
|
||||
|
||||
It is simpler during the sending, because it is possible to serialise signed part, prepend the signature and then send it.
|
||||
|
||||
It is more difficult during the receiving because to read the whole transmission it has to be parsed first (in order to have the size of the message in SEND command and to read the remaining binary), then serialized back, and only after it the signature can be verified. Possible solution is to have both unparsed and parsed transmission in some data structure (e.g. tuple) to avoid unnecessary serialisation.
|
||||
|
||||
The proposed change is also to change SMP protocol in this way:
|
||||
1. to have components of a signed transmission part on a single line, space-separated, rather than on multiple lines (to avoid byte-string differences because of different presentation of the end of line)
|
||||
2. the separation between SEND and binary part does not matter as binary is read separately and can be consistently appended with CRLF (arguably, it also applies to the rest of transmission part, but it is just simpler to read and parse one line and then append binary, if necessary, for signature verification).
|
||||
|
||||
```abnf
|
||||
transmission = [signature] CRLF signed CRLF
|
||||
signed = [corrId] SP [queueId] SP msg
|
||||
```
|
||||
|
||||
Off topic: SMP agent protocol can also be similarly changed to align with SMP protocol.
|
||||
|
||||
## Concerns
|
||||
|
||||
- MITM between SMP agent and server is still possible w/t transport encryption.
|
||||
@@ -0,0 +1,33 @@
|
||||
# SMP Agent: message management
|
||||
|
||||
The proposal is to change the way SMP agent manages the messages from the SMP servers.
|
||||
|
||||
Currently, it acts just as a proxy, when it does not receive any further messages from the server until the client ACKs them. The downside with this approach is that the agent can't work autonomously from the client, and the client always has to be active to retrieve the messages from the server. So while the agent offers a convenient, higher abstraction level when it comes to establishing connections, it does not offer any advantage over message management than using SMP server directly. There is a benefit to have an agent as a local, mailbox-like layer that downloads all messages for all connections and makes them available for further processing on the application layer.
|
||||
|
||||
## Phase 1. Managed agent MVP
|
||||
|
||||
These should be sufficient for terminal chat prototype
|
||||
|
||||
- Agent only maintains active subscriptions to the connections that the clients explicitly requested, only during client sessions, using `SUB` command.
|
||||
- Agent would automatically send `ACK` commands to SMP servers and store them in the database.
|
||||
- When the client subscribes to the connection (`SUB` - subscribe and deliver all messages received from the server) the response to `SUB` command is `OK`.
|
||||
- Receiving agent should maintain its own numbering in addition to the sending agent numbering, in case the sending agent message number decreases.
|
||||
- When there is an active subscription, Agent forwards messages to the connected clients in the order of their arrival, without any confirmations from the clients. It is client's responsibility to track that they didn't miss any messages and request re-delivery if needed.
|
||||
|
||||
## Phase 2. Subscription management and message access
|
||||
|
||||
- When the client subscribes to the connection (`SUB` - subscribe and deliver all missed messages) the response to `SUB` command is `NOMSG` or `MID n` where n is the ID of the last received message (1-based).
|
||||
- `SUB SKIP` - subscribe but only deliver the new messages
|
||||
- `SUB OFF` - turn off subscription
|
||||
- Messages received by the agent during the period when there was no active client subscription are not sent to the client automatically - client has to request them one by one using `MGET n` command (it can also be used to request re-delivery of old messages or for any other direct access scenario).
|
||||
- Clients can delete received messages using `MDEL n` command, where `n` is a sequential agent-assigned message ID - same that was used in `MSG` delivery.
|
||||
- Clients can confirm the receipt of the messages to the sender using `MACK n` command
|
||||
- Client can also request the last message ID with `MLAST` command - with the same responses.
|
||||
|
||||
## Phase 3. Autonomous agent
|
||||
|
||||
- Agent maintains active subscriptions to all connections that the clients created, whether there are active sessions with the clients or not.
|
||||
- `SUB ON` command would be used to receive messages from the agent, and while it is active the agent would maintain an active TCP connection(s) with the server(s) to retrieve the messages as soon as they arrive.
|
||||
- In the absence of the client connection the agent would periodically check and retrieve messages from the servers, possibly up to a quota, from all active connections. If the servers do not reply, the agent could have some retry logic with exponential back-off eventually marking connections (and/or servers) as unresponsive.
|
||||
|
||||
This design allows to have a single agent running in the background with multiple client applications connecting to it. It provides better message delivery guarantees, assuming transient nature of SMP server storage.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Streams
|
||||
|
||||
Managing dedicated SMP queues for fast synchronous communication
|
||||
|
||||
## Problem
|
||||
|
||||
SMP agent protocol implementation provides the most secure way to distribute keys achieving the following qualities:
|
||||
|
||||
- compromising the sender agent/device allows to send messages, but not to read them.
|
||||
- compromising the recipient agent/device allows to receive messages, but not to send them (neither public encryption key is stored - TBC if public key can be restored from the private - nor server authentication key is available).
|
||||
- compromising the server does not expose any information about messages content, as encrypted section has the same size.
|
||||
|
||||
The current hybrid encryption scheme uses a new symmetric key for each message, and because the symmetric key is not persisted at any point, it is difficult to obtain it and send counterfeit messages on behalf of the sender (even in case both the server and recipient are compromised).
|
||||
|
||||
There are 2 downsides of the current scheme:
|
||||
|
||||
1. RSA encryption/decryption is relatively slow even for 2048 key size and gets much slower for larger key sizes. It is not a problem for the chat messages (and any content updates) that happen infrequently, but it makes the transmission of large files and any other streaming communication slow.
|
||||
|
||||
2. If the large file or voice/video calls were to be sent via the same queue as normal messages/content updates, the server and any passive observer would be able to understand when such transmissions happen (even if performance was not a problem).
|
||||
|
||||
## Proposed solution
|
||||
|
||||
For every fast/dense data transmission use a separate short-lived connection with dedicated SMP queues that the agents would provision to receive such transmissions.
|
||||
|
||||
Such connections that can be unidirectional or duplex can be managed by agents via additional set of agent commands using "stream" abstraction.
|
||||
|
||||
The protocol to create and manage such stream could be the following:
|
||||
|
||||
1. The sender client notifies the agent about the stream it has to create for a specific duplex connection (the connection must be duplex for it to be possible): `<conn_alias> SNEW <size>` (where `conn_alias` is the existing duplex connection, `size` is the size for the data to be transferred; 0 for unbounded streams), the response is `<conn_alias> STREAM <stream_id> <size>`.
|
||||
2. The sender agent sends to the recipient agent the "envelope" `STREAM <stream_id> <size>`
|
||||
3. The receiving agent would notify the client about availability of the stream by sending `<conn_alias> STREAM <stream_id> <size>` notification.
|
||||
4. Receiving client would send `<conn_alias> SJOIN <stream_id>` command to the receiving agent.
|
||||
5. Receiving agent provisions the SMP queue on the random SMP server (we planned to remove SMP server from NEW command anyway). Possibly, there will be a limited list of servers that support streams (that is un-throttled number of messages), and stream support can be communicated via welcome header and change with AUTH SMP command.
|
||||
6. Receiving agent sends the envelop to sending agent `SJOIN <stream_id> <invitation>` (where invitation has the same format as in out-of-band message, but it has symmetric AES key and IV instead of asymmetric RSA key - the encryption scheme should codified in the invitation to make it generic).
|
||||
7. Sending agent and receiving agent confirm and secure SMP queue as usual. This is still not solving the problem that the sending agent should sign each command. We might extend SMP protocol to create a new queue type that allows authenticating the sender only once per TCP session so that messages can be sent without signing - the server can see that these queues are special anyway, as there will be much faster traffic there.
|
||||
8. Sending agent, once the queue is confirmed notifies the sending client that the stream is ready to accept data by sending notification `<conn_alias> SREADY <stream_id> <max_chunk_size>`.
|
||||
9. Sending agent client can now send packets into the stream up to total `<size>` limit if it was specified by using command `<conn_alias> SPUT <stream_id> <chunk_no> <chunk_size> <binary> ` (the chunk size should not be bigger than `max_chunk_size`, the agent would pad it to this size before sending to the receiving agent)
|
||||
10. Sending agent would send the chunk to the receiving agent in `CHUNK <chunk_no> <ts> <chunk_size> <binary>` envelope, where the first 4 bytes in decrypted binary is chunk size.
|
||||
11. Receiving agent would send the chunk to the client as `<conn_alias> SDATA <stream_id> <chunk_no> <ts> <status> <chunk_size> <binary>` (possibly, with three IDs and timestamps as with messages).
|
||||
12. Both the receiving and sending clients can terminate the stream with `<conn_alias> SCLOSE <stream_id>` command. The agent would send `SCLOSED <stream_id>` envelope and the opposite client would receive `<conn_alias> SCLOSED <stream_id>` notification - no more data will be accepted into this stream.
|
||||
13. If the stream was bounded with some `size` then both the sending and receiving client would receive `SCLOSED` notification after the last chunk of correct size was sent (and the incorrect chunk would be rejected).
|
||||
14. If the stream was interrupted then the recipient client can request resuming this stream using `<conn_alias> SRESUME <stream_id> <from_chunk_no>`, the recipient agent would send "envelope" `SRESUME <stream_id> <from_chunk_no> <invitation>`.
|
||||
|
||||
To be clarified:
|
||||
1. Possibly the internal stream ID used by clients should be different from stream ID communicated between agents, and in any case it should be connection scoped (same as with messages).
|
||||
2. There seems to be no need to rotate symmetric keys, disconnections can be handled.
|
||||
3. Same as we plan for redundant SMP queues with one connection, agents could use multiple queues per stream to increase the bandwidth and/or transmission reliability (e.g. for file transfers, where interruptions are not problematic multiple queues can be used in parallel, while for calls agents can duplicate all transmitted bytes so that losing some queues or TCP disconnects do not result in communication interruption).
|
||||
4. Possibly the stream creation could be initiated by the receiving agent, to have it more similar to connection creation procedure.
|
||||
@@ -0,0 +1,25 @@
|
||||
# SMP agent groups
|
||||
|
||||
## Problems
|
||||
|
||||
- device/user profile synchronisation
|
||||
- chat group communication
|
||||
|
||||
Both problems would require message broadcast between a group of SMP agents.
|
||||
|
||||
## Solution: basic symmetric groups via SMP agent protocol
|
||||
|
||||
Additional commands and message envelopes to SMP agent protocol to provide an abstraction layer for device synchronisation and chat groups.
|
||||
|
||||
The groups are fully symmetric, all agent who are members of the group have equal rights and can join and leave group at any time.
|
||||
|
||||
All the information about the groups is stored only in agents, the commands are used to synchronise the group state between the agents.
|
||||
|
||||
```abnf
|
||||
group_command = create_group / add_to_group / remove_from_group / leave_group
|
||||
group_response = group_created / added_to_group / removed_from_group
|
||||
group_notification = added_to_group_by / removed_from_group_by / left_group
|
||||
create_group = %s"GNEW " group_name ; cAlias must be empty
|
||||
add_to_group = %s"GADD " group_name ; cAlias is the connection to add to the group
|
||||
added_to_group = %s"GADDED " name ; cAlias is the connection added to the group
|
||||
```
|
||||
@@ -1,75 +0,0 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Env.STM where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import MsgStore.STM
|
||||
import Network.Socket (ServiceName)
|
||||
import Numeric.Natural
|
||||
import QueueStore.STM
|
||||
import Transmission
|
||||
import UnliftIO.STM
|
||||
|
||||
data Config = Config
|
||||
{ tcpPort :: ServiceName,
|
||||
tbqSize :: Natural,
|
||||
queueIdBytes :: Int,
|
||||
msgIdBytes :: Int
|
||||
}
|
||||
|
||||
data Env = Env
|
||||
{ config :: Config,
|
||||
server :: Server,
|
||||
queueStore :: QueueStore,
|
||||
msgStore :: STMMsgStore,
|
||||
idsDrg :: TVar ChaChaDRG
|
||||
}
|
||||
|
||||
data Server = Server
|
||||
{ subscribedQ :: TBQueue (RecipientId, Client),
|
||||
subscribers :: TVar (Map RecipientId Client)
|
||||
}
|
||||
|
||||
data Client = Client
|
||||
{ subscriptions :: TVar (Map RecipientId Sub),
|
||||
rcvQ :: TBQueue Signed,
|
||||
sndQ :: TBQueue Signed
|
||||
}
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread ThreadId
|
||||
|
||||
data Sub = Sub
|
||||
{ subThread :: SubscriptionThread,
|
||||
delivered :: TMVar ()
|
||||
}
|
||||
|
||||
newServer :: Natural -> STM Server
|
||||
newServer qSize = do
|
||||
subscribedQ <- newTBQueue qSize
|
||||
subscribers <- newTVar M.empty
|
||||
return Server {subscribedQ, subscribers}
|
||||
|
||||
newClient :: Natural -> STM Client
|
||||
newClient qSize = do
|
||||
subscriptions <- newTVar M.empty
|
||||
rcvQ <- newTBQueue qSize
|
||||
sndQ <- newTBQueue qSize
|
||||
return Client {subscriptions, rcvQ, sndQ}
|
||||
|
||||
newSubscription :: STM Sub
|
||||
newSubscription = do
|
||||
delivered <- newEmptyTMVar
|
||||
return Sub {subThread = NoSub, delivered}
|
||||
|
||||
newEnv :: (MonadUnliftIO m, MonadRandom m) => Config -> m Env
|
||||
newEnv config = do
|
||||
server <- atomically $ newServer (tbqSize config)
|
||||
queueStore <- atomically newQueueStore
|
||||
msgStore <- atomically newMsgStore
|
||||
idsDrg <- drgNew >>= newTVarIO
|
||||
return Env {config, server, queueStore, msgStore, idsDrg}
|
||||
-279
@@ -1,279 +0,0 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Server (runSMPServer) where
|
||||
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Time.Clock
|
||||
import Env.STM
|
||||
import MsgStore
|
||||
import MsgStore.STM (MsgQueue)
|
||||
import QueueStore
|
||||
import QueueStore.STM (QueueStore)
|
||||
import Transmission
|
||||
import Transport
|
||||
import UnliftIO.Async
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Exception
|
||||
import UnliftIO.IO
|
||||
import UnliftIO.STM
|
||||
|
||||
runSMPServer :: (MonadRandom m, MonadUnliftIO m) => Config -> m ()
|
||||
runSMPServer cfg@Config {tcpPort} = do
|
||||
env <- newEnv cfg
|
||||
runReaderT smpServer env
|
||||
where
|
||||
smpServer :: (MonadUnliftIO m, MonadReader Env m) => m ()
|
||||
smpServer = do
|
||||
s <- asks server
|
||||
race_ (runTCPServer tcpPort runClient) (serverThread s)
|
||||
|
||||
serverThread :: MonadUnliftIO m => Server -> m ()
|
||||
serverThread Server {subscribedQ, subscribers} = forever . atomically $ do
|
||||
(rId, clnt) <- readTBQueue subscribedQ
|
||||
cs <- readTVar subscribers
|
||||
case M.lookup rId cs of
|
||||
Just Client {rcvQ} -> writeTBQueue rcvQ (rId, Cmd SBroker END)
|
||||
Nothing -> return ()
|
||||
writeTVar subscribers $ M.insert rId clnt cs
|
||||
|
||||
runClient :: (MonadUnliftIO m, MonadReader Env m) => Handle -> m ()
|
||||
runClient h = do
|
||||
putLn h "Welcome to SMP"
|
||||
q <- asks $ tbqSize . config
|
||||
c <- atomically $ newClient q
|
||||
s <- asks server
|
||||
raceAny_ [send h c, client c s, receive h c]
|
||||
`finally` cancelSubscribers c
|
||||
|
||||
cancelSubscribers :: (MonadUnliftIO m) => Client -> m ()
|
||||
cancelSubscribers Client {subscriptions} =
|
||||
readTVarIO subscriptions >>= mapM_ cancelSub
|
||||
|
||||
cancelSub :: (MonadUnliftIO m) => Sub -> m ()
|
||||
cancelSub = \case
|
||||
Sub {subThread = SubThread t} -> killThread t
|
||||
_ -> return ()
|
||||
|
||||
raceAny_ :: MonadUnliftIO m => [m a] -> m ()
|
||||
raceAny_ = r []
|
||||
where
|
||||
r as (m : ms) = withAsync m $ \a -> r (a : as) ms
|
||||
r as [] = void $ waitAnyCancel as
|
||||
|
||||
receive :: (MonadUnliftIO m, MonadReader Env m) => Handle -> Client -> m ()
|
||||
receive h Client {rcvQ} = forever $ do
|
||||
(signature, (queueId, cmdOrError)) <- tGet fromClient h
|
||||
signed <- case cmdOrError of
|
||||
Left e -> return . mkResp queueId $ ERR e
|
||||
Right cmd -> verifyTransmission signature queueId cmd
|
||||
atomically $ writeTBQueue rcvQ signed
|
||||
|
||||
send :: MonadUnliftIO m => Handle -> Client -> m ()
|
||||
send h Client {sndQ} = forever $ do
|
||||
signed <- atomically $ readTBQueue sndQ
|
||||
tPut h (B.empty, signed)
|
||||
|
||||
mkResp :: QueueId -> Command 'Broker -> Signed
|
||||
mkResp queueId command = (queueId, Cmd SBroker command)
|
||||
|
||||
verifyTransmission :: forall m. (MonadUnliftIO m, MonadReader Env m) => Signature -> QueueId -> Cmd -> m Signed
|
||||
verifyTransmission signature queueId cmd = do
|
||||
(queueId,) <$> case cmd of
|
||||
Cmd SBroker _ -> return $ smpErr INTERNAL -- it can only be client command, because `fromClient` was used
|
||||
Cmd SRecipient (NEW _) -> return cmd
|
||||
Cmd SRecipient _ -> withQueueRec SRecipient $ verifySignature . recipientKey
|
||||
Cmd SSender (SEND _) -> withQueueRec SSender $ verifySend . senderKey
|
||||
where
|
||||
withQueueRec :: SParty (p :: Party) -> (QueueRec -> m Cmd) -> m Cmd
|
||||
withQueueRec party f = do
|
||||
st <- asks queueStore
|
||||
qr <- atomically $ getQueue st party queueId
|
||||
either (return . smpErr) f qr
|
||||
verifySend :: Maybe PublicKey -> m Cmd
|
||||
verifySend
|
||||
| B.null signature = return . maybe cmd (const authErr)
|
||||
| otherwise = maybe (return authErr) verifySignature
|
||||
-- TODO stub
|
||||
verifySignature :: PublicKey -> m Cmd
|
||||
verifySignature key = return $ if signature == key then cmd else authErr
|
||||
|
||||
smpErr e = Cmd SBroker $ ERR e
|
||||
authErr = smpErr AUTH
|
||||
|
||||
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => Client -> Server -> m ()
|
||||
client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= processCommand
|
||||
>>= atomically . writeTBQueue sndQ
|
||||
where
|
||||
processCommand :: Signed -> m Signed
|
||||
processCommand (queueId, cmd) = do
|
||||
st <- asks queueStore
|
||||
case cmd of
|
||||
Cmd SBroker END -> unsubscribeQueue $> (queueId, cmd)
|
||||
Cmd SBroker _ -> return (queueId, cmd)
|
||||
Cmd SSender (SEND msgBody) -> sendMessage st msgBody
|
||||
Cmd SRecipient command -> case command of
|
||||
NEW rKey -> createQueue st rKey
|
||||
SUB -> subscribeQueue queueId
|
||||
ACK -> acknowledgeMsg
|
||||
KEY sKey -> okResp <$> atomically (secureQueue st queueId sKey)
|
||||
OFF -> okResp <$> atomically (suspendQueue st queueId)
|
||||
DEL -> delQueueAndMsgs st
|
||||
where
|
||||
createQueue :: QueueStore -> RecipientKey -> m Signed
|
||||
createQueue st rKey = mkResp B.empty <$> addSubscribe
|
||||
where
|
||||
addSubscribe =
|
||||
addQueueRetry 3 >>= \case
|
||||
Left e -> return $ ERR e
|
||||
Right (rId, sId) -> subscribeQueue rId $> IDS rId sId
|
||||
|
||||
addQueueRetry :: Int -> m (Either ErrorType (RecipientId, SenderId))
|
||||
addQueueRetry 0 = return $ Left INTERNAL
|
||||
addQueueRetry n = do
|
||||
ids <- getIds
|
||||
atomically (addQueue st rKey ids) >>= \case
|
||||
Left DUPLICATE -> addQueueRetry $ n - 1
|
||||
Left e -> return $ Left e
|
||||
Right _ -> return $ Right ids
|
||||
|
||||
getIds :: m (RecipientId, SenderId)
|
||||
getIds = do
|
||||
n <- asks $ queueIdBytes . config
|
||||
liftM2 (,) (randomId n) (randomId n)
|
||||
|
||||
subscribeQueue :: RecipientId -> m Signed
|
||||
subscribeQueue rId =
|
||||
atomically (getSubscription rId) >>= deliverMessage tryPeekMsg rId
|
||||
|
||||
getSubscription :: RecipientId -> STM Sub
|
||||
getSubscription rId = do
|
||||
subs <- readTVar subscriptions
|
||||
case M.lookup rId subs of
|
||||
Just s -> tryTakeTMVar (delivered s) $> s
|
||||
Nothing -> do
|
||||
writeTBQueue subscribedQ (rId, clnt)
|
||||
s <- newSubscription
|
||||
writeTVar subscriptions $ M.insert rId s subs
|
||||
return s
|
||||
|
||||
unsubscribeQueue :: m ()
|
||||
unsubscribeQueue = do
|
||||
sub <- atomically . stateTVar subscriptions $
|
||||
\cs -> (M.lookup queueId cs, M.delete queueId cs)
|
||||
mapM_ cancelSub sub
|
||||
|
||||
acknowledgeMsg :: m Signed
|
||||
acknowledgeMsg =
|
||||
atomically (withSub queueId $ \s -> const s <$$> tryTakeTMVar (delivered s))
|
||||
>>= \case
|
||||
Just (Just s) -> deliverMessage tryDelPeekMsg queueId s
|
||||
_ -> return $ err PROHIBITED
|
||||
|
||||
withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a)
|
||||
withSub rId f = readTVar subscriptions >>= mapM f . M.lookup rId
|
||||
|
||||
sendMessage :: QueueStore -> MsgBody -> m Signed
|
||||
sendMessage st msgBody = do
|
||||
qr <- atomically $ getQueue st SSender queueId
|
||||
either (return . err) storeMessage qr
|
||||
where
|
||||
mkMessage :: m Message
|
||||
mkMessage = do
|
||||
msgId <- asks (msgIdBytes . config) >>= randomId
|
||||
ts <- liftIO getCurrentTime
|
||||
return $ Message {msgId, ts, msgBody}
|
||||
|
||||
storeMessage :: QueueRec -> m Signed
|
||||
storeMessage qr = case status qr of
|
||||
QueueOff -> return $ err AUTH
|
||||
QueueActive -> do
|
||||
ms <- asks msgStore
|
||||
msg <- mkMessage
|
||||
atomically $ do
|
||||
q <- getMsgQueue ms (recipientId qr)
|
||||
writeMsg q msg
|
||||
return ok
|
||||
|
||||
deliverMessage :: (MsgQueue -> STM (Maybe Message)) -> RecipientId -> Sub -> m Signed
|
||||
deliverMessage tryPeek rId = \case
|
||||
Sub {subThread = NoSub} -> do
|
||||
ms <- asks msgStore
|
||||
q <- atomically $ getMsgQueue ms rId
|
||||
atomically (tryPeek q) >>= \case
|
||||
Nothing -> forkSub q $> ok
|
||||
Just msg -> atomically setDelivered $> msgResp rId msg
|
||||
_ -> return ok
|
||||
where
|
||||
forkSub :: MsgQueue -> m ()
|
||||
forkSub q = do
|
||||
atomically . setSub $ \s -> s {subThread = SubPending}
|
||||
t <- forkIO $ subscriber q
|
||||
atomically . setSub $ \case
|
||||
s@Sub {subThread = SubPending} -> s {subThread = SubThread t}
|
||||
s -> s
|
||||
|
||||
subscriber :: MsgQueue -> m ()
|
||||
subscriber q = atomically $ do
|
||||
msg <- peekMsg q
|
||||
writeTBQueue sndQ $ msgResp rId msg
|
||||
setSub (\s -> s {subThread = NoSub})
|
||||
void setDelivered
|
||||
|
||||
setSub :: (Sub -> Sub) -> STM ()
|
||||
setSub f = modifyTVar subscriptions $ M.adjust f rId
|
||||
|
||||
setDelivered :: STM (Maybe Bool)
|
||||
setDelivered = withSub rId $ \s -> tryPutTMVar (delivered s) ()
|
||||
|
||||
delQueueAndMsgs :: QueueStore -> m Signed
|
||||
delQueueAndMsgs st = do
|
||||
ms <- asks msgStore
|
||||
atomically $
|
||||
deleteQueue st queueId >>= \case
|
||||
Left e -> return $ err e
|
||||
Right _ -> delMsgQueue ms queueId $> ok
|
||||
|
||||
ok :: Signed
|
||||
ok = mkResp queueId OK
|
||||
|
||||
err :: ErrorType -> Signed
|
||||
err = mkResp queueId . ERR
|
||||
|
||||
okResp :: Either ErrorType () -> Signed
|
||||
okResp = either err $ const ok
|
||||
|
||||
msgResp :: RecipientId -> Message -> Signed
|
||||
msgResp rId Message {msgId, ts, msgBody} = mkResp rId $ MSG msgId ts msgBody
|
||||
|
||||
randomId :: (MonadUnliftIO m, MonadReader Env m) => Int -> m Encoded
|
||||
randomId n = do
|
||||
gVar <- asks idsDrg
|
||||
atomically (randomBytes n gVar)
|
||||
|
||||
randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString
|
||||
randomBytes n gVar = do
|
||||
g <- readTVar gVar
|
||||
let (bytes, g') = randomBytesGenerate n g
|
||||
writeTVar gVar g'
|
||||
return bytes
|
||||
@@ -0,0 +1,138 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Markdown where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import System.Console.ANSI.Types
|
||||
|
||||
data Markdown = Markdown Format Text | Markdown :|: Markdown
|
||||
deriving (Eq, Show)
|
||||
|
||||
data Format
|
||||
= Bold
|
||||
| Italic
|
||||
| Underline
|
||||
| StrikeThrough
|
||||
| Snippet
|
||||
| Secret
|
||||
| Colored Color
|
||||
| NoFormat
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Semigroup Markdown where (<>) = (:|:)
|
||||
|
||||
instance Monoid Markdown where mempty = unmarked ""
|
||||
|
||||
instance IsString Markdown where fromString = unmarked . T.pack
|
||||
|
||||
unmarked :: Text -> Markdown
|
||||
unmarked = Markdown NoFormat
|
||||
|
||||
colorMD :: Char
|
||||
colorMD = '!'
|
||||
|
||||
secretMD :: Char
|
||||
secretMD = '#'
|
||||
|
||||
formats :: Map Char Format
|
||||
formats =
|
||||
M.fromList
|
||||
[ ('*', Bold),
|
||||
('_', Italic),
|
||||
('+', Underline),
|
||||
('~', StrikeThrough),
|
||||
('`', Snippet),
|
||||
(secretMD, Secret),
|
||||
(colorMD, Colored White)
|
||||
]
|
||||
|
||||
colors :: Map Text Color
|
||||
colors =
|
||||
M.fromList
|
||||
[ ("red", Red),
|
||||
("green", Green),
|
||||
("blue", Blue),
|
||||
("yellow", Yellow),
|
||||
("cyan", Cyan),
|
||||
("magenta", Magenta),
|
||||
("r", Red),
|
||||
("g", Green),
|
||||
("b", Blue),
|
||||
("y", Yellow),
|
||||
("c", Cyan),
|
||||
("m", Magenta),
|
||||
("1", Red),
|
||||
("2", Green),
|
||||
("3", Blue),
|
||||
("4", Yellow),
|
||||
("5", Cyan),
|
||||
("6", Magenta)
|
||||
]
|
||||
|
||||
parseMarkdown :: Text -> Markdown
|
||||
parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s
|
||||
|
||||
markdownP :: Parser Markdown
|
||||
markdownP = merge <$> A.many' fragmentP
|
||||
where
|
||||
merge :: [Markdown] -> Markdown
|
||||
merge [] = ""
|
||||
merge fs = foldr1 (:|:) fs
|
||||
fragmentP :: Parser Markdown
|
||||
fragmentP =
|
||||
A.anyChar >>= \case
|
||||
' ' -> unmarked . T.cons ' ' <$> A.takeWhile (== ' ')
|
||||
c -> case M.lookup c formats of
|
||||
Just Secret -> secretP
|
||||
Just (Colored White) -> coloredP
|
||||
Just f -> formattedP c "" f
|
||||
Nothing -> unformattedP c
|
||||
formattedP :: Char -> Text -> Format -> Parser Markdown
|
||||
formattedP c p f = do
|
||||
s <- A.takeTill (== c)
|
||||
(A.char c $> markdown c p f s) <|> noFormat (c `T.cons` p <> s)
|
||||
markdown :: Char -> Text -> Format -> Text -> Markdown
|
||||
markdown c p f s
|
||||
| T.null s || T.head s == ' ' || T.last s == ' ' =
|
||||
unmarked $ c `T.cons` p <> s `T.snoc` c
|
||||
| otherwise = Markdown f s
|
||||
secretP :: Parser Markdown
|
||||
secretP = secret <$> A.takeWhile (== secretMD) <*> A.takeTill (== secretMD) <*> A.takeWhile (== secretMD)
|
||||
secret :: Text -> Text -> Text -> Markdown
|
||||
secret b s a
|
||||
| T.null a || T.null s || T.head s == ' ' || T.last s == ' ' =
|
||||
unmarked $ secretMD `T.cons` ss
|
||||
| otherwise = Markdown Secret $ T.init ss
|
||||
where
|
||||
ss = b <> s <> a
|
||||
coloredP :: Parser Markdown
|
||||
coloredP = do
|
||||
color <- A.takeWhile (\c -> c /= ' ' && c /= colorMD)
|
||||
case M.lookup color colors of
|
||||
Just c ->
|
||||
let f = Colored c
|
||||
in (A.char ' ' *> formattedP colorMD (color `T.snoc` ' ') f)
|
||||
<|> noFormat (colorMD `T.cons` color)
|
||||
_ -> noFormat (colorMD `T.cons` color)
|
||||
unformattedP :: Char -> Parser Markdown
|
||||
unformattedP c = unmarked . T.cons c <$> wordsP
|
||||
wordsP :: Parser Text
|
||||
wordsP = do
|
||||
s <- (<>) <$> A.takeTill (== ' ') <*> A.takeWhile (== ' ')
|
||||
A.peekChar >>= \case
|
||||
Nothing -> pure s
|
||||
Just c -> case M.lookup c formats of
|
||||
Just _ -> pure s
|
||||
Nothing -> (s <>) <$> wordsP
|
||||
noFormat :: Text -> Parser Markdown
|
||||
noFormat = pure . unmarked
|
||||
@@ -0,0 +1,358 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent
|
||||
( runSMPAgent,
|
||||
runSMPAgentBlocking,
|
||||
getSMPAgentClient,
|
||||
runSMPAgentClient,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple (logInfo, showText)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (MonadRandom)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Data.Time.Clock
|
||||
import Database.SQLite.Simple (SQLError)
|
||||
import Simplex.Messaging.Agent.Client
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, connectSQLiteStore)
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Client (SMPServerTransmission)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (CorrId (..), MsgBody, SenderPublicKey)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport (putLn, runTCPServer)
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import System.IO (Handle)
|
||||
import UnliftIO.Async (race_)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m ()
|
||||
runSMPAgent cfg = newEmptyTMVarIO >>= (`runSMPAgentBlocking` cfg)
|
||||
|
||||
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => TMVar Bool -> AgentConfig -> m ()
|
||||
runSMPAgentBlocking started cfg@AgentConfig {tcpPort} = runReaderT smpAgent =<< newSMPAgentEnv cfg
|
||||
where
|
||||
smpAgent :: (MonadUnliftIO m', MonadReader Env m') => m' ()
|
||||
smpAgent = runTCPServer started tcpPort $ \h -> do
|
||||
liftIO $ putLn h "Welcome to SMP v0.2.0 agent"
|
||||
c <- getSMPAgentClient
|
||||
logConnection c True
|
||||
race_ (connectClient h c) (runSMPAgentClient c)
|
||||
`E.finally` (closeSMPServerClients c >> logConnection c False)
|
||||
|
||||
getSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient
|
||||
getSMPAgentClient = do
|
||||
n <- asks clientCounter
|
||||
cfg <- asks config
|
||||
atomically $ newAgentClient n cfg
|
||||
|
||||
connectClient :: MonadUnliftIO m => Handle -> AgentClient -> m ()
|
||||
connectClient h c = race_ (send h c) (receive h c)
|
||||
|
||||
logConnection :: MonadUnliftIO m => AgentClient -> Bool -> m ()
|
||||
logConnection c connected =
|
||||
let event = if connected then "connected to" else "disconnected from"
|
||||
in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"]
|
||||
|
||||
runSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
|
||||
runSMPAgentClient c = do
|
||||
db <- asks $ dbFile . config
|
||||
s1 <- connectSQLiteStore db
|
||||
s2 <- connectSQLiteStore db
|
||||
race_ (subscriber c s1) (client c s2)
|
||||
|
||||
receive :: forall m. MonadUnliftIO m => Handle -> AgentClient -> m ()
|
||||
receive h c@AgentClient {rcvQ, sndQ} = forever $ do
|
||||
(corrId, cAlias, cmdOrErr) <- tGet SClient h
|
||||
case cmdOrErr of
|
||||
Right cmd -> write rcvQ (corrId, cAlias, cmd)
|
||||
Left e -> write sndQ (corrId, cAlias, ERR e)
|
||||
where
|
||||
write :: TBQueue (ATransmission p) -> ATransmission p -> m ()
|
||||
write q t = do
|
||||
logClient c "-->" t
|
||||
atomically $ writeTBQueue q t
|
||||
|
||||
send :: MonadUnliftIO m => Handle -> AgentClient -> m ()
|
||||
send h c@AgentClient {sndQ} = forever $ do
|
||||
t <- atomically $ readTBQueue sndQ
|
||||
tPut h t
|
||||
logClient c "<--" t
|
||||
|
||||
logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m ()
|
||||
logClient AgentClient {clientId} dir (CorrId corrId, cAlias, cmd) = do
|
||||
logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, cAlias, B.takeWhile (/= ' ') $ serializeCommand cmd]
|
||||
|
||||
client :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()
|
||||
client c@AgentClient {rcvQ, sndQ} st = forever $ do
|
||||
t@(corrId, cAlias, _) <- atomically $ readTBQueue rcvQ
|
||||
runExceptT (processCommand c st t) >>= \case
|
||||
Left e -> atomically $ writeTBQueue sndQ (corrId, cAlias, ERR e)
|
||||
Right _ -> return ()
|
||||
|
||||
withStore ::
|
||||
AgentMonad m =>
|
||||
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => m' a) ->
|
||||
m a
|
||||
withStore action = do
|
||||
runExceptT (action `E.catch` handleInternal) >>= \case
|
||||
Right c -> return c
|
||||
Left e -> throwError $ storeError e
|
||||
where
|
||||
handleInternal :: (MonadError StoreError m') => SQLError -> m' a
|
||||
handleInternal e = throwError . SEInternal $ bshow e
|
||||
storeError :: StoreError -> AgentErrorType
|
||||
storeError = \case
|
||||
SEConnNotFound -> CONN UNKNOWN
|
||||
SEConnDuplicate -> CONN DUPLICATE
|
||||
e -> INTERNAL $ show e
|
||||
|
||||
processCommand :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ATransmission 'Client -> m ()
|
||||
processCommand c@AgentClient {sndQ} st (corrId, connAlias, cmd) =
|
||||
case cmd of
|
||||
NEW smpServer -> createNewConnection smpServer
|
||||
JOIN smpQueueInfo replyMode -> joinConnection smpQueueInfo replyMode
|
||||
SUB -> subscribeConnection connAlias
|
||||
SUBALL -> subscribeAll
|
||||
SEND msgBody -> sendMessage msgBody
|
||||
OFF -> suspendConnection
|
||||
DEL -> deleteConnection
|
||||
where
|
||||
createNewConnection :: SMPServer -> m ()
|
||||
createNewConnection server = do
|
||||
-- TODO create connection alias if not passed
|
||||
-- make connAlias Maybe?
|
||||
(rq, qInfo) <- newReceiveQueue c server connAlias
|
||||
withStore $ createRcvConn st rq
|
||||
respond $ INV qInfo
|
||||
|
||||
joinConnection :: SMPQueueInfo -> ReplyMode -> m ()
|
||||
joinConnection qInfo@(SMPQueueInfo srv _ _) replyMode = do
|
||||
-- TODO create connection alias if not passed
|
||||
-- make connAlias Maybe?
|
||||
(sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias
|
||||
withStore $ createSndConn st sq
|
||||
connectToSendQueue c st sq senderKey verifyKey
|
||||
case replyMode of
|
||||
ReplyOn -> sendReplyQInfo srv sq
|
||||
ReplyVia srv' -> sendReplyQInfo srv' sq
|
||||
ReplyOff -> return ()
|
||||
respond CON
|
||||
|
||||
subscribeConnection :: ConnAlias -> m ()
|
||||
subscribeConnection cAlias =
|
||||
withStore (getConn st cAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> subscribe rq
|
||||
SomeConn _ (RcvConnection _ rq) -> subscribe rq
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
where
|
||||
subscribe rq = subscribeQueue c rq cAlias >> respond' cAlias OK
|
||||
|
||||
-- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct
|
||||
subscribeAll :: m ()
|
||||
subscribeAll = withStore (getAllConnAliases st) >>= mapM_ subscribeConnection
|
||||
|
||||
sendMessage :: MsgBody -> m ()
|
||||
sendMessage msgBody =
|
||||
withStore (getConn st connAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq
|
||||
SomeConn _ (SndConnection _ sq) -> sendMsg sq
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
where
|
||||
sendMsg sq = do
|
||||
internalTs <- liftIO getCurrentTime
|
||||
(internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st sq
|
||||
let msgStr =
|
||||
serializeSMPMessage
|
||||
SMPMessage
|
||||
{ senderMsgId = unSndId internalSndId,
|
||||
senderTimestamp = internalTs,
|
||||
previousMsgHash,
|
||||
agentMessage = A_MSG msgBody
|
||||
}
|
||||
msgHash = C.sha256Hash msgStr
|
||||
withStore $
|
||||
createSndMsg st sq $
|
||||
SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash}
|
||||
sendAgentMessage c sq msgStr
|
||||
respond $ SENT (unId internalId)
|
||||
|
||||
suspendConnection :: m ()
|
||||
suspendConnection =
|
||||
withStore (getConn st connAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> suspend rq
|
||||
SomeConn _ (RcvConnection _ rq) -> suspend rq
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
where
|
||||
suspend rq = suspendQueue c rq >> respond OK
|
||||
|
||||
deleteConnection :: m ()
|
||||
deleteConnection =
|
||||
withStore (getConn st connAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> delete rq
|
||||
SomeConn _ (RcvConnection _ rq) -> delete rq
|
||||
_ -> delConn
|
||||
where
|
||||
delConn = withStore (deleteConn st connAlias) >> respond OK
|
||||
delete rq = do
|
||||
deleteQueue c rq
|
||||
removeSubscription c connAlias
|
||||
delConn
|
||||
|
||||
sendReplyQInfo :: SMPServer -> SndQueue -> m ()
|
||||
sendReplyQInfo srv sq = do
|
||||
(rq, qInfo) <- newReceiveQueue c srv connAlias
|
||||
withStore $ upgradeSndConnToDuplex st connAlias rq
|
||||
senderTimestamp <- liftIO getCurrentTime
|
||||
sendAgentMessage c sq . serializeSMPMessage $
|
||||
SMPMessage
|
||||
{ senderMsgId = 0,
|
||||
senderTimestamp,
|
||||
previousMsgHash = "",
|
||||
agentMessage = REPLY qInfo
|
||||
}
|
||||
|
||||
respond :: ACommand 'Agent -> m ()
|
||||
respond = respond' connAlias
|
||||
|
||||
respond' :: ConnAlias -> ACommand 'Agent -> m ()
|
||||
respond' cAlias resp = atomically $ writeTBQueue sndQ (corrId, cAlias, resp)
|
||||
|
||||
subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()
|
||||
subscriber c@AgentClient {msgQ} st = forever $ do
|
||||
-- TODO this will only process messages and notifications
|
||||
t <- atomically $ readTBQueue msgQ
|
||||
runExceptT (processSMPTransmission c st t) >>= \case
|
||||
Left e -> liftIO $ print e
|
||||
Right _ -> return ()
|
||||
|
||||
processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> SMPServerTransmission -> m ()
|
||||
processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do
|
||||
rq@RcvQueue {connAlias, status} <- withStore $ getRcvQueue st srv rId
|
||||
case cmd of
|
||||
SMP.MSG srvMsgId srvTs msgBody -> do
|
||||
-- TODO deduplicate with previously received
|
||||
msg <- decryptAndVerify rq msgBody
|
||||
let msgHash = C.sha256Hash msg
|
||||
agentMsg <- liftEither $ parseSMPMessage msg
|
||||
case agentMsg of
|
||||
SMPConfirmation senderKey -> do
|
||||
logServer "<--" c srv rId "MSG <KEY>"
|
||||
case status of
|
||||
New -> do
|
||||
-- TODO currently it automatically allows whoever sends the confirmation
|
||||
-- Commands CONF and LET are not supported in v0.2
|
||||
withStore $ setRcvQueueStatus st rq Confirmed
|
||||
-- TODO update sender key in the store?
|
||||
secureQueue c rq senderKey
|
||||
withStore $ setRcvQueueStatus st rq Secured
|
||||
_ -> notify connAlias . ERR $ AGENT A_PROHIBITED
|
||||
SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} ->
|
||||
case agentMessage of
|
||||
HELLO verifyKey _ -> do
|
||||
logServer "<--" c srv rId "MSG <HELLO>"
|
||||
case status of
|
||||
Active -> notify connAlias . ERR $ AGENT A_PROHIBITED
|
||||
_ -> do
|
||||
void $ verifyMessage (Just verifyKey) msgBody
|
||||
withStore $ setRcvQueueActive st rq verifyKey
|
||||
REPLY qInfo -> do
|
||||
logServer "<--" c srv rId "MSG <REPLY>"
|
||||
(sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias
|
||||
withStore $ upgradeRcvConnToDuplex st connAlias sq
|
||||
connectToSendQueue c st sq senderKey verifyKey
|
||||
notify connAlias CON
|
||||
A_MSG body -> agentClientMsg rq previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash
|
||||
sendAck c rq
|
||||
return ()
|
||||
SMP.END -> do
|
||||
removeSubscription c connAlias
|
||||
logServer "<--" c srv rId "END"
|
||||
notify connAlias END
|
||||
_ -> do
|
||||
logServer "<--" c srv rId $ "unexpected: " <> bshow cmd
|
||||
notify connAlias . ERR $ BROKER UNEXPECTED
|
||||
where
|
||||
notify :: ConnAlias -> ACommand 'Agent -> m ()
|
||||
notify connAlias msg = atomically $ writeTBQueue sndQ ("", connAlias, msg)
|
||||
agentClientMsg :: RcvQueue -> PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m ()
|
||||
agentClientMsg rq@RcvQueue {connAlias, status} receivedPrevMsgHash senderMeta brokerMeta msgBody msgHash = do
|
||||
logServer "<--" c srv rId "MSG <MSG>"
|
||||
case status of
|
||||
Active -> do
|
||||
internalTs <- liftIO getCurrentTime
|
||||
(internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore $ updateRcvIds st rq
|
||||
let msgIntegrity = checkMsgIntegrity prevExtSndId (fst senderMeta) prevRcvMsgHash
|
||||
withStore $
|
||||
createRcvMsg st rq $
|
||||
RcvMsgData
|
||||
{ internalId,
|
||||
internalRcvId,
|
||||
internalTs,
|
||||
senderMeta,
|
||||
brokerMeta,
|
||||
msgBody,
|
||||
internalHash = msgHash,
|
||||
externalPrevSndHash = receivedPrevMsgHash,
|
||||
msgIntegrity
|
||||
}
|
||||
notify connAlias $
|
||||
MSG
|
||||
{ recipientMeta = (unId internalId, internalTs),
|
||||
senderMeta,
|
||||
brokerMeta,
|
||||
msgBody,
|
||||
msgIntegrity
|
||||
}
|
||||
_ -> notify connAlias . ERR $ AGENT A_PROHIBITED
|
||||
where
|
||||
checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> MsgIntegrity
|
||||
checkMsgIntegrity prevExtSndId extSndId internalPrevMsgHash
|
||||
| extSndId == prevExtSndId + 1 && internalPrevMsgHash == receivedPrevMsgHash = MsgOk
|
||||
| extSndId < prevExtSndId = MsgError $ MsgBadId extSndId
|
||||
| extSndId == prevExtSndId = MsgError MsgDuplicate -- ? deduplicate
|
||||
| extSndId > prevExtSndId + 1 = MsgError $ MsgSkipped (prevExtSndId + 1) (extSndId - 1)
|
||||
| internalPrevMsgHash /= receivedPrevMsgHash = MsgError MsgBadHash
|
||||
| otherwise = MsgError MsgDuplicate -- this case is not possible
|
||||
|
||||
connectToSendQueue :: AgentMonad m => AgentClient -> SQLiteStore -> SndQueue -> SenderPublicKey -> VerificationKey -> m ()
|
||||
connectToSendQueue c st sq senderKey verifyKey = do
|
||||
sendConfirmation c sq senderKey
|
||||
withStore $ setSndQueueStatus st sq Confirmed
|
||||
sendHello c sq verifyKey
|
||||
withStore $ setSndQueueStatus st sq Active
|
||||
|
||||
newSendQueue ::
|
||||
(MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> ConnAlias -> m (SndQueue, SenderPublicKey, VerificationKey)
|
||||
newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do
|
||||
size <- asks $ rsaKeySize . config
|
||||
(senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size
|
||||
(verifyKey, signKey) <- liftIO $ C.generateKeyPair size
|
||||
let sndQueue =
|
||||
SndQueue
|
||||
{ server = smpServer,
|
||||
sndId = senderId,
|
||||
connAlias,
|
||||
sndPrivateKey,
|
||||
encryptKey,
|
||||
signKey,
|
||||
status = New
|
||||
}
|
||||
return (sndQueue, senderKey, verifyKey)
|
||||
@@ -0,0 +1,312 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Client
|
||||
( AgentClient (..),
|
||||
newAgentClient,
|
||||
AgentMonad,
|
||||
getSMPServerClient,
|
||||
closeSMPServerClients,
|
||||
newReceiveQueue,
|
||||
subscribeQueue,
|
||||
sendConfirmation,
|
||||
sendHello,
|
||||
secureQueue,
|
||||
sendAgentMessage,
|
||||
decryptAndVerify,
|
||||
verifyMessage,
|
||||
sendAck,
|
||||
suspendQueue,
|
||||
deleteQueue,
|
||||
logServer,
|
||||
removeSubscription,
|
||||
cryptoError,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text.Encoding
|
||||
import Data.Time.Clock
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgBody, QueueId, SenderPublicKey)
|
||||
import Simplex.Messaging.Util (bshow, liftEitherError, liftError)
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Exception (IOException)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
data AgentClient = AgentClient
|
||||
{ rcvQ :: TBQueue (ATransmission 'Client),
|
||||
sndQ :: TBQueue (ATransmission 'Agent),
|
||||
msgQ :: TBQueue SMPServerTransmission,
|
||||
smpClients :: TVar (Map SMPServer SMPClient),
|
||||
subscrSrvrs :: TVar (Map SMPServer (Set ConnAlias)),
|
||||
subscrConns :: TVar (Map ConnAlias SMPServer),
|
||||
clientId :: Int
|
||||
}
|
||||
|
||||
newAgentClient :: TVar Int -> AgentConfig -> STM AgentClient
|
||||
newAgentClient cc AgentConfig {tbqSize} = do
|
||||
rcvQ <- newTBQueue tbqSize
|
||||
sndQ <- newTBQueue tbqSize
|
||||
msgQ <- newTBQueue tbqSize
|
||||
smpClients <- newTVar M.empty
|
||||
subscrSrvrs <- newTVar M.empty
|
||||
subscrConns <- newTVar M.empty
|
||||
clientId <- (+ 1) <$> readTVar cc
|
||||
writeTVar cc clientId
|
||||
return AgentClient {rcvQ, sndQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId}
|
||||
|
||||
type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m)
|
||||
|
||||
getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient
|
||||
getSMPServerClient c@AgentClient {smpClients, msgQ} srv =
|
||||
readTVarIO smpClients
|
||||
>>= maybe newSMPClient return . M.lookup srv
|
||||
where
|
||||
newSMPClient :: m SMPClient
|
||||
newSMPClient = do
|
||||
smp <- connectClient
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
|
||||
atomically . modifyTVar smpClients $ M.insert srv smp
|
||||
return smp
|
||||
|
||||
connectClient :: m SMPClient
|
||||
connectClient = do
|
||||
cfg <- asks $ smpCfg . config
|
||||
liftEitherError smpClientError (getSMPClient srv cfg msgQ clientDisconnected)
|
||||
`E.catch` internalError
|
||||
where
|
||||
internalError :: IOException -> m SMPClient
|
||||
internalError = throwError . INTERNAL . show
|
||||
|
||||
clientDisconnected :: IO ()
|
||||
clientDisconnected = do
|
||||
removeSubs >>= mapM_ (mapM_ notifySub)
|
||||
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
||||
|
||||
removeSubs :: IO (Maybe (Set ConnAlias))
|
||||
removeSubs = atomically $ do
|
||||
modifyTVar smpClients $ M.delete srv
|
||||
cs <- M.lookup srv <$> readTVar (subscrSrvrs c)
|
||||
modifyTVar (subscrSrvrs c) $ M.delete srv
|
||||
modifyTVar (subscrConns c) $ maybe id deleteKeys cs
|
||||
return cs
|
||||
where
|
||||
deleteKeys :: Ord k => Set k -> Map k a -> Map k a
|
||||
deleteKeys ks m = S.foldr' M.delete m ks
|
||||
|
||||
notifySub :: ConnAlias -> IO ()
|
||||
notifySub connAlias = atomically $ writeTBQueue (sndQ c) ("", connAlias, END)
|
||||
|
||||
closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m ()
|
||||
closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient
|
||||
|
||||
withSMP_ :: forall a m. AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> m a) -> m a
|
||||
withSMP_ c srv action =
|
||||
(getSMPServerClient c srv >>= action) `catchError` logServerError
|
||||
where
|
||||
logServerError :: AgentErrorType -> m a
|
||||
logServerError e = do
|
||||
logServer "<--" c srv "" $ bshow e
|
||||
throwError e
|
||||
|
||||
withLogSMP_ :: AgentMonad m => AgentClient -> SMPServer -> QueueId -> ByteString -> (SMPClient -> m a) -> m a
|
||||
withLogSMP_ c srv qId cmdStr action = do
|
||||
logServer "-->" c srv qId cmdStr
|
||||
res <- withSMP_ c srv action
|
||||
logServer "<--" c srv qId "OK"
|
||||
return res
|
||||
|
||||
withSMP :: AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> ExceptT SMPClientError IO a) -> m a
|
||||
withSMP c srv action = withSMP_ c srv $ liftSMP . action
|
||||
|
||||
withLogSMP :: AgentMonad m => AgentClient -> SMPServer -> QueueId -> ByteString -> (SMPClient -> ExceptT SMPClientError IO a) -> m a
|
||||
withLogSMP c srv qId cmdStr action = withLogSMP_ c srv qId cmdStr $ liftSMP . action
|
||||
|
||||
liftSMP :: AgentMonad m => ExceptT SMPClientError IO a -> m a
|
||||
liftSMP = liftError smpClientError
|
||||
|
||||
smpClientError :: SMPClientError -> AgentErrorType
|
||||
smpClientError = \case
|
||||
SMPServerError e -> SMP e
|
||||
SMPResponseError e -> BROKER $ RESPONSE e
|
||||
SMPUnexpectedResponse -> BROKER UNEXPECTED
|
||||
SMPResponseTimeout -> BROKER TIMEOUT
|
||||
SMPNetworkError -> BROKER NETWORK
|
||||
SMPTransportError e -> BROKER $ TRANSPORT e
|
||||
e -> INTERNAL $ show e
|
||||
|
||||
newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> ConnAlias -> m (RcvQueue, SMPQueueInfo)
|
||||
newReceiveQueue c srv connAlias = do
|
||||
size <- asks $ rsaKeySize . config
|
||||
(recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size
|
||||
logServer "-->" c srv "" "NEW"
|
||||
(rcvId, sId) <- withSMP c srv $ \smp -> createSMPQueue smp rcvPrivateKey recipientKey
|
||||
logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sId]
|
||||
(encryptKey, decryptKey) <- liftIO $ C.generateKeyPair size
|
||||
let rq =
|
||||
RcvQueue
|
||||
{ server = srv,
|
||||
rcvId,
|
||||
connAlias,
|
||||
rcvPrivateKey,
|
||||
sndId = Just sId,
|
||||
sndKey = Nothing,
|
||||
decryptKey,
|
||||
verifyKey = Nothing,
|
||||
status = New
|
||||
}
|
||||
addSubscription c rq connAlias
|
||||
return (rq, SMPQueueInfo srv sId encryptKey)
|
||||
|
||||
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnAlias -> m ()
|
||||
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connAlias = do
|
||||
withLogSMP c server rcvId "SUB" $ \smp ->
|
||||
subscribeSMPQueue smp rcvPrivateKey rcvId
|
||||
addSubscription c rq connAlias
|
||||
|
||||
addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnAlias -> m ()
|
||||
addSubscription c RcvQueue {server} connAlias = atomically $ do
|
||||
modifyTVar (subscrConns c) $ M.insert connAlias server
|
||||
modifyTVar (subscrSrvrs c) $ M.alter (Just . addSub) server
|
||||
where
|
||||
addSub :: Maybe (Set ConnAlias) -> Set ConnAlias
|
||||
addSub (Just cs) = S.insert connAlias cs
|
||||
addSub _ = S.singleton connAlias
|
||||
|
||||
removeSubscription :: AgentMonad m => AgentClient -> ConnAlias -> m ()
|
||||
removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically $ do
|
||||
cs <- readTVar subscrConns
|
||||
writeTVar subscrConns $ M.delete connAlias cs
|
||||
mapM_
|
||||
(modifyTVar subscrSrvrs . M.alter (>>= delSub))
|
||||
(M.lookup connAlias cs)
|
||||
where
|
||||
delSub :: Set ConnAlias -> Maybe (Set ConnAlias)
|
||||
delSub cs =
|
||||
let cs' = S.delete connAlias cs
|
||||
in if S.null cs' then Nothing else Just cs'
|
||||
|
||||
logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m ()
|
||||
logServer dir AgentClient {clientId} srv qId cmdStr =
|
||||
logInfo . decodeUtf8 $ B.unwords ["A", "(" <> bshow clientId <> ")", dir, showServer srv, ":", logSecret qId, cmdStr]
|
||||
|
||||
showServer :: SMPServer -> ByteString
|
||||
showServer srv = B.pack $ host srv <> maybe "" (":" <>) (port srv)
|
||||
|
||||
logSecret :: ByteString -> ByteString
|
||||
logSecret bs = encode $ B.take 3 bs
|
||||
|
||||
sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> m ()
|
||||
sendConfirmation c sq@SndQueue {server, sndId} senderKey =
|
||||
withLogSMP_ c server sndId "SEND <KEY>" $ \smp -> do
|
||||
msg <- mkConfirmation smp
|
||||
liftSMP $ sendSMPMessage smp Nothing sndId msg
|
||||
where
|
||||
mkConfirmation :: SMPClient -> m MsgBody
|
||||
mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey
|
||||
|
||||
sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> m ()
|
||||
sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey =
|
||||
withLogSMP_ c server sndId "SEND <HELLO> (retrying)" $ \smp -> do
|
||||
msg <- mkHello smp $ AckMode On
|
||||
liftSMP $ send 8 100000 msg smp
|
||||
where
|
||||
mkHello :: SMPClient -> AckMode -> m ByteString
|
||||
mkHello smp ackMode = do
|
||||
senderTimestamp <- liftIO getCurrentTime
|
||||
encryptAndSign smp sq . serializeSMPMessage $
|
||||
SMPMessage
|
||||
{ senderMsgId = 0,
|
||||
senderTimestamp,
|
||||
previousMsgHash = "",
|
||||
agentMessage = HELLO verifyKey ackMode
|
||||
}
|
||||
|
||||
send :: Int -> Int -> ByteString -> SMPClient -> ExceptT SMPClientError IO ()
|
||||
send 0 _ _ _ = throwE $ SMPServerError AUTH
|
||||
send retry delay msg smp =
|
||||
sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case
|
||||
SMPServerError AUTH -> do
|
||||
threadDelay delay
|
||||
send (retry - 1) (delay * 3 `div` 2) msg smp
|
||||
e -> throwE e
|
||||
|
||||
secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m ()
|
||||
secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey =
|
||||
withLogSMP c server rcvId "KEY <key>" $ \smp ->
|
||||
secureSMPQueue smp rcvPrivateKey rcvId senderKey
|
||||
|
||||
sendAck :: AgentMonad m => AgentClient -> RcvQueue -> m ()
|
||||
sendAck c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
withLogSMP c server rcvId "ACK" $ \smp ->
|
||||
ackSMPMessage smp rcvPrivateKey rcvId
|
||||
|
||||
suspendQueue :: AgentMonad m => AgentClient -> RcvQueue -> m ()
|
||||
suspendQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
withLogSMP c server rcvId "OFF" $ \smp ->
|
||||
suspendSMPQueue smp rcvPrivateKey rcvId
|
||||
|
||||
deleteQueue :: AgentMonad m => AgentClient -> RcvQueue -> m ()
|
||||
deleteQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
withLogSMP c server rcvId "DEL" $ \smp ->
|
||||
deleteSMPQueue smp rcvPrivateKey rcvId
|
||||
|
||||
sendAgentMessage :: AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
|
||||
sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} msg =
|
||||
withLogSMP_ c server sndId "SEND <message>" $ \smp -> do
|
||||
msg' <- encryptAndSign smp sq msg
|
||||
liftSMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msg'
|
||||
|
||||
encryptAndSign :: AgentMonad m => SMPClient -> SndQueue -> ByteString -> m ByteString
|
||||
encryptAndSign smp SndQueue {encryptKey, signKey} msg = do
|
||||
paddedSize <- asks $ (blockSize smp -) . reservedMsgSize
|
||||
liftError cryptoError $ do
|
||||
enc <- C.encrypt encryptKey paddedSize msg
|
||||
C.Signature sig <- C.sign signKey enc
|
||||
pure $ sig <> enc
|
||||
|
||||
decryptAndVerify :: AgentMonad m => RcvQueue -> ByteString -> m ByteString
|
||||
decryptAndVerify RcvQueue {decryptKey, verifyKey} msg =
|
||||
verifyMessage verifyKey msg
|
||||
>>= liftError cryptoError . C.decrypt decryptKey
|
||||
|
||||
verifyMessage :: AgentMonad m => Maybe VerificationKey -> ByteString -> m ByteString
|
||||
verifyMessage verifyKey msg = do
|
||||
size <- asks $ rsaKeySize . config
|
||||
let (sig, enc) = B.splitAt size msg
|
||||
case verifyKey of
|
||||
Nothing -> pure enc
|
||||
Just k
|
||||
| C.verify k (C.Signature sig) enc -> pure enc
|
||||
| otherwise -> throwError $ AGENT A_SIGNATURE
|
||||
|
||||
cryptoError :: C.CryptoError -> AgentErrorType
|
||||
cryptoError = \case
|
||||
C.CryptoLargeMsgError -> CMD LARGE
|
||||
C.RSADecryptError _ -> AGENT A_ENCRYPTION
|
||||
C.CryptoHeaderError _ -> AGENT A_ENCRYPTION
|
||||
C.AESDecryptError -> AGENT A_ENCRYPTION
|
||||
e -> INTERNAL $ show e
|
||||
@@ -0,0 +1,43 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Env.SQLite where
|
||||
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Network.Socket
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import Simplex.Messaging.Client
|
||||
import UnliftIO.STM
|
||||
|
||||
data AgentConfig = AgentConfig
|
||||
{ tcpPort :: ServiceName,
|
||||
rsaKeySize :: Int,
|
||||
connIdBytes :: Int,
|
||||
tbqSize :: Natural,
|
||||
dbFile :: FilePath,
|
||||
smpCfg :: SMPClientConfig
|
||||
}
|
||||
|
||||
data Env = Env
|
||||
{ config :: AgentConfig,
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
clientCounter :: TVar Int,
|
||||
reservedMsgSize :: Int
|
||||
}
|
||||
|
||||
newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env
|
||||
newSMPAgentEnv config = do
|
||||
idsDrg <- drgNew >>= newTVarIO
|
||||
_ <- createSQLiteStore $ dbFile config
|
||||
clientCounter <- newTVarIO 0
|
||||
return Env {config, idsDrg, clientCounter, reservedMsgSize}
|
||||
where
|
||||
-- 1st rsaKeySize is used by the RSA signature in each command,
|
||||
-- 2nd - by encrypted message body header
|
||||
-- 3rd - by message signature
|
||||
-- smpCommandSize - is the estimated max size for SMP command, queueId, corrId
|
||||
reservedMsgSize = 3 * rsaKeySize config + smpCommandSize (smpCfg config)
|
||||
@@ -0,0 +1,281 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Type.Equality
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Protocol
|
||||
( MsgBody,
|
||||
MsgId,
|
||||
RecipientPrivateKey,
|
||||
SenderPrivateKey,
|
||||
SenderPublicKey,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
|
||||
-- * Store management
|
||||
|
||||
-- | Store class type. Defines store access methods for implementations.
|
||||
class Monad m => MonadAgentStore s m where
|
||||
-- Queue and Connection management
|
||||
createRcvConn :: s -> RcvQueue -> m ()
|
||||
createSndConn :: s -> SndQueue -> m ()
|
||||
getConn :: s -> ConnAlias -> m SomeConn
|
||||
getAllConnAliases :: s -> m [ConnAlias] -- TODO remove - hack for subscribing to all
|
||||
getRcvQueue :: s -> SMPServer -> SMP.RecipientId -> m RcvQueue
|
||||
deleteConn :: s -> ConnAlias -> m ()
|
||||
upgradeRcvConnToDuplex :: s -> ConnAlias -> SndQueue -> m ()
|
||||
upgradeSndConnToDuplex :: s -> ConnAlias -> RcvQueue -> m ()
|
||||
setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m ()
|
||||
setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m ()
|
||||
setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m ()
|
||||
|
||||
-- Msg management
|
||||
updateRcvIds :: s -> RcvQueue -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
|
||||
createRcvMsg :: s -> RcvQueue -> RcvMsgData -> m ()
|
||||
|
||||
updateSndIds :: s -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
createSndMsg :: s -> SndQueue -> SndMsgData -> m ()
|
||||
|
||||
getMsg :: s -> ConnAlias -> InternalId -> m Msg
|
||||
|
||||
-- * Queue types
|
||||
|
||||
-- | A receive queue. SMP queue through which the agent receives messages from a sender.
|
||||
data RcvQueue = RcvQueue
|
||||
{ server :: SMPServer,
|
||||
rcvId :: SMP.RecipientId,
|
||||
connAlias :: ConnAlias,
|
||||
rcvPrivateKey :: RecipientPrivateKey,
|
||||
sndId :: Maybe SMP.SenderId,
|
||||
sndKey :: Maybe SenderPublicKey,
|
||||
decryptKey :: DecryptionKey,
|
||||
verifyKey :: Maybe VerificationKey,
|
||||
status :: QueueStatus
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | A send queue. SMP queue through which the agent sends messages to a recipient.
|
||||
data SndQueue = SndQueue
|
||||
{ server :: SMPServer,
|
||||
sndId :: SMP.SenderId,
|
||||
connAlias :: ConnAlias,
|
||||
sndPrivateKey :: SenderPrivateKey,
|
||||
encryptKey :: EncryptionKey,
|
||||
signKey :: SignatureKey,
|
||||
status :: QueueStatus
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- * Connection types
|
||||
|
||||
-- | Type of a connection.
|
||||
data ConnType = CRcv | CSnd | CDuplex deriving (Eq, Show)
|
||||
|
||||
-- | Connection of a specific type.
|
||||
--
|
||||
-- - RcvConnection is a connection that only has a receive queue set up,
|
||||
-- typically created by a recipient initiating a duplex connection.
|
||||
--
|
||||
-- - SndConnection is a connection that only has a send queue set up, typically
|
||||
-- created by a sender joining a duplex connection through a recipient's invitation.
|
||||
--
|
||||
-- - DuplexConnection is a connection that has both receive and send queues set up,
|
||||
-- typically created by upgrading a receive or a send connection with a missing queue.
|
||||
data Connection (d :: ConnType) where
|
||||
RcvConnection :: ConnAlias -> RcvQueue -> Connection CRcv
|
||||
SndConnection :: ConnAlias -> SndQueue -> Connection CSnd
|
||||
DuplexConnection :: ConnAlias -> RcvQueue -> SndQueue -> Connection CDuplex
|
||||
|
||||
deriving instance Eq (Connection d)
|
||||
|
||||
deriving instance Show (Connection d)
|
||||
|
||||
data SConnType :: ConnType -> Type where
|
||||
SCRcv :: SConnType CRcv
|
||||
SCSnd :: SConnType CSnd
|
||||
SCDuplex :: SConnType CDuplex
|
||||
|
||||
connType :: SConnType c -> ConnType
|
||||
connType SCRcv = CRcv
|
||||
connType SCSnd = CSnd
|
||||
connType SCDuplex = CDuplex
|
||||
|
||||
deriving instance Eq (SConnType d)
|
||||
|
||||
deriving instance Show (SConnType d)
|
||||
|
||||
instance TestEquality SConnType where
|
||||
testEquality SCRcv SCRcv = Just Refl
|
||||
testEquality SCSnd SCSnd = Just Refl
|
||||
testEquality SCDuplex SCDuplex = Just Refl
|
||||
testEquality _ _ = Nothing
|
||||
|
||||
-- | Connection of an unknown type.
|
||||
-- Used to refer to an arbitrary connection when retrieving from store.
|
||||
data SomeConn = forall d. SomeConn (SConnType d) (Connection d)
|
||||
|
||||
instance Eq SomeConn where
|
||||
SomeConn d c == SomeConn d' c' = case testEquality d d' of
|
||||
Just Refl -> c == c'
|
||||
_ -> False
|
||||
|
||||
deriving instance Show SomeConn
|
||||
|
||||
-- * Message integrity validation types
|
||||
|
||||
type MsgHash = ByteString
|
||||
|
||||
-- | Corresponds to `last_external_snd_msg_id` in `connections` table
|
||||
type PrevExternalSndId = Int64
|
||||
|
||||
-- | Corresponds to `last_rcv_msg_hash` in `connections` table
|
||||
type PrevRcvMsgHash = MsgHash
|
||||
|
||||
-- | Corresponds to `last_snd_msg_hash` in `connections` table
|
||||
type PrevSndMsgHash = MsgHash
|
||||
|
||||
-- ? merge/replace these with RcvMsg and SndMsg
|
||||
-- * Message data containers - used on Msg creation to reduce number of parameters
|
||||
|
||||
data RcvMsgData = RcvMsgData
|
||||
{ internalId :: InternalId,
|
||||
internalRcvId :: InternalRcvId,
|
||||
internalTs :: InternalTs,
|
||||
senderMeta :: (ExternalSndId, ExternalSndTs),
|
||||
brokerMeta :: (BrokerId, BrokerTs),
|
||||
msgBody :: MsgBody,
|
||||
internalHash :: MsgHash,
|
||||
externalPrevSndHash :: MsgHash,
|
||||
msgIntegrity :: MsgIntegrity
|
||||
}
|
||||
|
||||
data SndMsgData = SndMsgData
|
||||
{ internalId :: InternalId,
|
||||
internalSndId :: InternalSndId,
|
||||
internalTs :: InternalTs,
|
||||
msgBody :: MsgBody,
|
||||
internalHash :: MsgHash
|
||||
}
|
||||
|
||||
-- * Message types
|
||||
|
||||
-- | A message in either direction that is stored by the agent.
|
||||
data Msg = MRcv RcvMsg | MSnd SndMsg
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | A message received by the agent from a sender.
|
||||
data RcvMsg = RcvMsg
|
||||
{ msgBase :: MsgBase,
|
||||
internalRcvId :: InternalRcvId,
|
||||
-- | Id of the message at sender, corresponds to `internalSndId` from the sender's side.
|
||||
-- Sender Id is made sequential for detection of missing messages. For redundant / parallel queues,
|
||||
-- it also allows to keep track of duplicates and restore the original order before delivery to the client.
|
||||
externalSndId :: ExternalSndId,
|
||||
externalSndTs :: ExternalSndTs,
|
||||
-- | Id of the message at broker, although it is not sequential (to avoid metadata leakage for potential observer),
|
||||
-- it is needed to track repeated deliveries in case of connection loss - this logic is not implemented yet.
|
||||
brokerId :: BrokerId,
|
||||
brokerTs :: BrokerTs,
|
||||
rcvMsgStatus :: RcvMsgStatus,
|
||||
-- | Timestamp of acknowledgement to broker, corresponds to `AcknowledgedToBroker` status.
|
||||
-- Do not mix up with `brokerTs` - timestamp created at broker after it receives the message from sender.
|
||||
ackBrokerTs :: AckBrokerTs,
|
||||
-- | Timestamp of acknowledgement to sender, corresponds to `AcknowledgedToSender` status.
|
||||
-- Do not mix up with `externalSndTs` - timestamp created at sender before sending,
|
||||
-- which in its turn corresponds to `internalTs` in sending agent.
|
||||
ackSenderTs :: AckSenderTs,
|
||||
-- | Hash of previous message as received from sender - stored for integrity forensics.
|
||||
externalPrevSndHash :: MsgHash,
|
||||
msgIntegrity :: MsgIntegrity
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- internal Ids are newtypes to prevent mixing them up
|
||||
newtype InternalRcvId = InternalRcvId {unRcvId :: Int64} deriving (Eq, Show)
|
||||
|
||||
type ExternalSndId = Int64
|
||||
|
||||
type ExternalSndTs = UTCTime
|
||||
|
||||
type BrokerId = MsgId
|
||||
|
||||
type BrokerTs = UTCTime
|
||||
|
||||
data RcvMsgStatus
|
||||
= Received
|
||||
| AcknowledgedToBroker
|
||||
| AcknowledgedToSender
|
||||
deriving (Eq, Show)
|
||||
|
||||
type AckBrokerTs = UTCTime
|
||||
|
||||
type AckSenderTs = UTCTime
|
||||
|
||||
-- | A message sent by the agent to a recipient.
|
||||
data SndMsg = SndMsg
|
||||
{ msgBase :: MsgBase,
|
||||
-- | Id of the message sent / to be sent, as in its number in order of sending.
|
||||
internalSndId :: InternalSndId,
|
||||
sndMsgStatus :: SndMsgStatus,
|
||||
-- | Timestamp of the message received by broker, corresponds to `Sent` status.
|
||||
sentTs :: SentTs,
|
||||
-- | Timestamp of the message received by recipient, corresponds to `Delivered` status.
|
||||
deliveredTs :: DeliveredTs
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
newtype InternalSndId = InternalSndId {unSndId :: Int64} deriving (Eq, Show)
|
||||
|
||||
data SndMsgStatus
|
||||
= Created
|
||||
| Sent
|
||||
| Delivered
|
||||
deriving (Eq, Show)
|
||||
|
||||
type SentTs = UTCTime
|
||||
|
||||
type DeliveredTs = UTCTime
|
||||
|
||||
-- | Base message data independent of direction.
|
||||
data MsgBase = MsgBase
|
||||
{ connAlias :: ConnAlias,
|
||||
-- | Monotonically increasing id of a message per connection, internal to the agent.
|
||||
-- Internal Id preserves ordering between both received and sent messages, and is needed
|
||||
-- to track the order of the conversation (which can be different for the sender / receiver)
|
||||
-- and address messages in commands. External [sender] Id cannot be used for this purpose
|
||||
-- due to a possibility of implementation errors in different agents.
|
||||
internalId :: InternalId,
|
||||
internalTs :: InternalTs,
|
||||
msgBody :: MsgBody,
|
||||
-- | Hash of the message as computed by agent.
|
||||
internalHash :: MsgHash
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
newtype InternalId = InternalId {unId :: Int64} deriving (Eq, Show)
|
||||
|
||||
type InternalTs = UTCTime
|
||||
|
||||
-- * Store errors
|
||||
|
||||
data StoreError
|
||||
= SEInternal ByteString
|
||||
| SEConnNotFound
|
||||
| SEConnDuplicate
|
||||
| SEBadConnType ConnType
|
||||
| SEBadQueueStatus -- not used, planned to check strictly
|
||||
| SENotImplemented -- TODO remove
|
||||
deriving (Eq, Show, Exception)
|
||||
@@ -0,0 +1,654 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite
|
||||
( SQLiteStore (..),
|
||||
createSQLiteStore,
|
||||
connectSQLiteStore,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO))
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.List (find)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (isPrefixOf)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Database.SQLite.Simple (FromRow, NamedParam (..), SQLData (..), SQLError, field)
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.Internal (Field (..))
|
||||
import Database.SQLite.Simple.Ok (Ok (Ok))
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Network.Socket (ServiceName)
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Schema (createSchema)
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Parsers (blobFieldParser)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Util (bshow, liftIOEither)
|
||||
import System.Exit (ExitCode (ExitFailure), exitWith)
|
||||
import System.FilePath (takeDirectory)
|
||||
import Text.Read (readMaybe)
|
||||
import UnliftIO.Directory (createDirectoryIfMissing)
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
-- * SQLite Store implementation
|
||||
|
||||
data SQLiteStore = SQLiteStore
|
||||
{ dbFilePath :: FilePath,
|
||||
dbConn :: DB.Connection
|
||||
}
|
||||
|
||||
createSQLiteStore :: MonadUnliftIO m => FilePath -> m SQLiteStore
|
||||
createSQLiteStore dbFilePath = do
|
||||
let dbDir = takeDirectory dbFilePath
|
||||
createDirectoryIfMissing False dbDir
|
||||
store <- connectSQLiteStore dbFilePath
|
||||
compileOptions <- liftIO (DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]])
|
||||
let threadsafeOption = find (isPrefixOf "THREADSAFE=") (concat compileOptions)
|
||||
liftIO $ case threadsafeOption of
|
||||
Just "THREADSAFE=0" -> do
|
||||
putStrLn "SQLite compiled with not threadsafe code, continue (y/n):"
|
||||
s <- getLine
|
||||
when (s /= "y") (exitWith $ ExitFailure 2)
|
||||
Nothing -> putStrLn "Warning: SQLite THREADSAFE compile option not found"
|
||||
_ -> return ()
|
||||
liftIO . createSchema $ dbConn store
|
||||
return store
|
||||
|
||||
connectSQLiteStore :: MonadUnliftIO m => FilePath -> m SQLiteStore
|
||||
connectSQLiteStore dbFilePath = do
|
||||
dbConn <- liftIO $ DB.open dbFilePath
|
||||
liftIO $ DB.execute_ dbConn "PRAGMA foreign_keys = ON;"
|
||||
return SQLiteStore {dbFilePath, dbConn}
|
||||
|
||||
checkDuplicate :: (MonadUnliftIO m, MonadError StoreError m) => IO () -> m ()
|
||||
checkDuplicate action = liftIOEither $ first handleError <$> E.try action
|
||||
where
|
||||
handleError :: SQLError -> StoreError
|
||||
handleError e
|
||||
| DB.sqlError e == DB.ErrorConstraint = SEConnDuplicate
|
||||
| otherwise = SEInternal $ bshow e
|
||||
|
||||
instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteStore m where
|
||||
createRcvConn :: SQLiteStore -> RcvQueue -> m ()
|
||||
createRcvConn SQLiteStore {dbConn} q@RcvQueue {server} =
|
||||
checkDuplicate $
|
||||
DB.withTransaction dbConn $ do
|
||||
upsertServer_ dbConn server
|
||||
insertRcvQueue_ dbConn q
|
||||
insertRcvConnection_ dbConn q
|
||||
|
||||
createSndConn :: SQLiteStore -> SndQueue -> m ()
|
||||
createSndConn SQLiteStore {dbConn} q@SndQueue {server} =
|
||||
checkDuplicate $
|
||||
DB.withTransaction dbConn $ do
|
||||
upsertServer_ dbConn server
|
||||
insertSndQueue_ dbConn q
|
||||
insertSndConnection_ dbConn q
|
||||
|
||||
getConn :: SQLiteStore -> ConnAlias -> m SomeConn
|
||||
getConn SQLiteStore {dbConn} connAlias =
|
||||
liftIOEither . DB.withTransaction dbConn $
|
||||
getConn_ dbConn connAlias
|
||||
|
||||
getAllConnAliases :: SQLiteStore -> m [ConnAlias]
|
||||
getAllConnAliases SQLiteStore {dbConn} =
|
||||
liftIO $ do
|
||||
r <- DB.query_ dbConn "SELECT conn_alias FROM connections;" :: IO [[ConnAlias]]
|
||||
return (concat r)
|
||||
|
||||
getRcvQueue :: SQLiteStore -> SMPServer -> SMP.RecipientId -> m RcvQueue
|
||||
getRcvQueue SQLiteStore {dbConn} SMPServer {host, port} rcvId = do
|
||||
r <-
|
||||
liftIO $
|
||||
DB.queryNamed
|
||||
dbConn
|
||||
[sql|
|
||||
SELECT
|
||||
s.key_hash, q.host, q.port, q.rcv_id, q.conn_alias, q.rcv_private_key,
|
||||
q.snd_id, q.snd_key, q.decrypt_key, q.verify_key, q.status
|
||||
FROM rcv_queues q
|
||||
INNER JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
WHERE q.host = :host AND q.port = :port AND q.rcv_id = :rcv_id;
|
||||
|]
|
||||
[":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId]
|
||||
case r of
|
||||
[(keyHash, hst, prt, rId, connAlias, rcvPrivateKey, sndId, sndKey, decryptKey, verifyKey, status)] ->
|
||||
let srv = SMPServer hst (deserializePort_ prt) keyHash
|
||||
in pure $ RcvQueue srv rId connAlias rcvPrivateKey sndId sndKey decryptKey verifyKey status
|
||||
_ -> throwError SEConnNotFound
|
||||
|
||||
deleteConn :: SQLiteStore -> ConnAlias -> m ()
|
||||
deleteConn SQLiteStore {dbConn} connAlias =
|
||||
liftIO $
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
"DELETE FROM connections WHERE conn_alias = :conn_alias;"
|
||||
[":conn_alias" := connAlias]
|
||||
|
||||
upgradeRcvConnToDuplex :: SQLiteStore -> ConnAlias -> SndQueue -> m ()
|
||||
upgradeRcvConnToDuplex SQLiteStore {dbConn} connAlias sq@SndQueue {server} =
|
||||
liftIOEither . DB.withTransaction dbConn $
|
||||
getConn_ dbConn connAlias >>= \case
|
||||
Right (SomeConn SCRcv (RcvConnection _ _)) -> do
|
||||
upsertServer_ dbConn server
|
||||
insertSndQueue_ dbConn sq
|
||||
updateConnWithSndQueue_ dbConn connAlias sq
|
||||
pure $ Right ()
|
||||
Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c
|
||||
_ -> pure $ Left SEConnNotFound
|
||||
|
||||
upgradeSndConnToDuplex :: SQLiteStore -> ConnAlias -> RcvQueue -> m ()
|
||||
upgradeSndConnToDuplex SQLiteStore {dbConn} connAlias rq@RcvQueue {server} =
|
||||
liftIOEither . DB.withTransaction dbConn $
|
||||
getConn_ dbConn connAlias >>= \case
|
||||
Right (SomeConn SCSnd (SndConnection _ _)) -> do
|
||||
upsertServer_ dbConn server
|
||||
insertRcvQueue_ dbConn rq
|
||||
updateConnWithRcvQueue_ dbConn connAlias rq
|
||||
pure $ Right ()
|
||||
Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c
|
||||
_ -> pure $ Left SEConnNotFound
|
||||
|
||||
setRcvQueueStatus :: SQLiteStore -> RcvQueue -> QueueStatus -> m ()
|
||||
setRcvQueueStatus SQLiteStore {dbConn} RcvQueue {rcvId, server = SMPServer {host, port}} status =
|
||||
-- ? throw error if queue doesn't exist?
|
||||
liftIO $
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
UPDATE rcv_queues
|
||||
SET status = :status
|
||||
WHERE host = :host AND port = :port AND rcv_id = :rcv_id;
|
||||
|]
|
||||
[":status" := status, ":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId]
|
||||
|
||||
setRcvQueueActive :: SQLiteStore -> RcvQueue -> VerificationKey -> m ()
|
||||
setRcvQueueActive SQLiteStore {dbConn} RcvQueue {rcvId, server = SMPServer {host, port}} verifyKey =
|
||||
-- ? throw error if queue doesn't exist?
|
||||
liftIO $
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
UPDATE rcv_queues
|
||||
SET verify_key = :verify_key, status = :status
|
||||
WHERE host = :host AND port = :port AND rcv_id = :rcv_id;
|
||||
|]
|
||||
[ ":verify_key" := Just verifyKey,
|
||||
":status" := Active,
|
||||
":host" := host,
|
||||
":port" := serializePort_ port,
|
||||
":rcv_id" := rcvId
|
||||
]
|
||||
|
||||
setSndQueueStatus :: SQLiteStore -> SndQueue -> QueueStatus -> m ()
|
||||
setSndQueueStatus SQLiteStore {dbConn} SndQueue {sndId, server = SMPServer {host, port}} status =
|
||||
-- ? throw error if queue doesn't exist?
|
||||
liftIO $
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
UPDATE snd_queues
|
||||
SET status = :status
|
||||
WHERE host = :host AND port = :port AND snd_id = :snd_id;
|
||||
|]
|
||||
[":status" := status, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId]
|
||||
|
||||
updateRcvIds :: SQLiteStore -> RcvQueue -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
|
||||
updateRcvIds SQLiteStore {dbConn} RcvQueue {connAlias} =
|
||||
liftIO . DB.withTransaction dbConn $ do
|
||||
(lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ dbConn connAlias
|
||||
let internalId = InternalId $ unId lastInternalId + 1
|
||||
internalRcvId = InternalRcvId $ unRcvId lastInternalRcvId + 1
|
||||
updateLastIdsRcv_ dbConn connAlias internalId internalRcvId
|
||||
pure (internalId, internalRcvId, lastExternalSndId, lastRcvHash)
|
||||
|
||||
createRcvMsg :: SQLiteStore -> RcvQueue -> RcvMsgData -> m ()
|
||||
createRcvMsg SQLiteStore {dbConn} RcvQueue {connAlias} rcvMsgData =
|
||||
liftIO . DB.withTransaction dbConn $ do
|
||||
insertRcvMsgBase_ dbConn connAlias rcvMsgData
|
||||
insertRcvMsgDetails_ dbConn connAlias rcvMsgData
|
||||
updateHashRcv_ dbConn connAlias rcvMsgData
|
||||
|
||||
updateSndIds :: SQLiteStore -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
updateSndIds SQLiteStore {dbConn} SndQueue {connAlias} =
|
||||
liftIO . DB.withTransaction dbConn $ do
|
||||
(lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ dbConn connAlias
|
||||
let internalId = InternalId $ unId lastInternalId + 1
|
||||
internalSndId = InternalSndId $ unSndId lastInternalSndId + 1
|
||||
updateLastIdsSnd_ dbConn connAlias internalId internalSndId
|
||||
pure (internalId, internalSndId, prevSndHash)
|
||||
|
||||
createSndMsg :: SQLiteStore -> SndQueue -> SndMsgData -> m ()
|
||||
createSndMsg SQLiteStore {dbConn} SndQueue {connAlias} sndMsgData =
|
||||
liftIO . DB.withTransaction dbConn $ do
|
||||
insertSndMsgBase_ dbConn connAlias sndMsgData
|
||||
insertSndMsgDetails_ dbConn connAlias sndMsgData
|
||||
updateHashSnd_ dbConn connAlias sndMsgData
|
||||
|
||||
getMsg :: SQLiteStore -> ConnAlias -> InternalId -> m Msg
|
||||
getMsg _st _connAlias _id = throwError SENotImplemented
|
||||
|
||||
-- * Auxiliary helpers
|
||||
|
||||
-- ? replace with ToField? - it's easy to forget to use this
|
||||
serializePort_ :: Maybe ServiceName -> ServiceName
|
||||
serializePort_ = fromMaybe "_"
|
||||
|
||||
deserializePort_ :: ServiceName -> Maybe ServiceName
|
||||
deserializePort_ "_" = Nothing
|
||||
deserializePort_ port = Just port
|
||||
|
||||
instance ToField QueueStatus where toField = toField . show
|
||||
|
||||
instance FromField QueueStatus where fromField = fromFieldToReadable_
|
||||
|
||||
instance ToField InternalRcvId where toField (InternalRcvId x) = toField x
|
||||
|
||||
instance FromField InternalRcvId where fromField x = InternalRcvId <$> fromField x
|
||||
|
||||
instance ToField InternalSndId where toField (InternalSndId x) = toField x
|
||||
|
||||
instance FromField InternalSndId where fromField x = InternalSndId <$> fromField x
|
||||
|
||||
instance ToField InternalId where toField (InternalId x) = toField x
|
||||
|
||||
instance FromField InternalId where fromField x = InternalId <$> fromField x
|
||||
|
||||
instance ToField RcvMsgStatus where toField = toField . show
|
||||
|
||||
instance ToField SndMsgStatus where toField = toField . show
|
||||
|
||||
instance ToField MsgIntegrity where toField = toField . serializeMsgIntegrity
|
||||
|
||||
instance FromField MsgIntegrity where fromField = blobFieldParser msgIntegrityP
|
||||
|
||||
fromFieldToReadable_ :: forall a. (Read a, E.Typeable a) => Field -> Ok a
|
||||
fromFieldToReadable_ = \case
|
||||
f@(Field (SQLText t) _) ->
|
||||
let str = T.unpack t
|
||||
in case readMaybe str of
|
||||
Just x -> Ok x
|
||||
_ -> returnError ConversionFailed f ("invalid string: " <> str)
|
||||
f -> returnError ConversionFailed f "expecting SQLText column type"
|
||||
|
||||
{- 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
|
||||
{- ORMOLU_ENABLE -}
|
||||
|
||||
-- * Server upsert helper
|
||||
|
||||
upsertServer_ :: DB.Connection -> SMPServer -> IO ()
|
||||
upsertServer_ dbConn SMPServer {host, port, keyHash} = do
|
||||
let port_ = serializePort_ port
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO servers (host, port, key_hash) VALUES (:host,:port,:key_hash)
|
||||
ON CONFLICT (host, port) DO UPDATE SET
|
||||
host=excluded.host,
|
||||
port=excluded.port,
|
||||
key_hash=excluded.key_hash;
|
||||
|]
|
||||
[":host" := host, ":port" := port_, ":key_hash" := keyHash]
|
||||
|
||||
-- * createRcvConn helpers
|
||||
|
||||
insertRcvQueue_ :: DB.Connection -> RcvQueue -> IO ()
|
||||
insertRcvQueue_ dbConn RcvQueue {..} = do
|
||||
let port_ = serializePort_ $ port server
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO rcv_queues
|
||||
( host, port, rcv_id, conn_alias, rcv_private_key, snd_id, snd_key, decrypt_key, verify_key, status)
|
||||
VALUES
|
||||
(:host,:port,:rcv_id,:conn_alias,:rcv_private_key,:snd_id,:snd_key,:decrypt_key,:verify_key,:status);
|
||||
|]
|
||||
[ ":host" := host server,
|
||||
":port" := port_,
|
||||
":rcv_id" := rcvId,
|
||||
":conn_alias" := connAlias,
|
||||
":rcv_private_key" := rcvPrivateKey,
|
||||
":snd_id" := sndId,
|
||||
":snd_key" := sndKey,
|
||||
":decrypt_key" := decryptKey,
|
||||
":verify_key" := verifyKey,
|
||||
":status" := status
|
||||
]
|
||||
|
||||
insertRcvConnection_ :: DB.Connection -> RcvQueue -> IO ()
|
||||
insertRcvConnection_ dbConn RcvQueue {server, rcvId, connAlias} = do
|
||||
let port_ = serializePort_ $ port server
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO connections
|
||||
( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id,
|
||||
last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id,
|
||||
last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash)
|
||||
VALUES
|
||||
(:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL,
|
||||
0, 0, 0, 0, x'', x'');
|
||||
|]
|
||||
[":conn_alias" := connAlias, ":rcv_host" := host server, ":rcv_port" := port_, ":rcv_id" := rcvId]
|
||||
|
||||
-- * createSndConn helpers
|
||||
|
||||
insertSndQueue_ :: DB.Connection -> SndQueue -> IO ()
|
||||
insertSndQueue_ dbConn SndQueue {..} = do
|
||||
let port_ = serializePort_ $ port server
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO snd_queues
|
||||
( host, port, snd_id, conn_alias, snd_private_key, encrypt_key, sign_key, status)
|
||||
VALUES
|
||||
(:host,:port,:snd_id,:conn_alias,:snd_private_key,:encrypt_key,:sign_key,:status);
|
||||
|]
|
||||
[ ":host" := host server,
|
||||
":port" := port_,
|
||||
":snd_id" := sndId,
|
||||
":conn_alias" := connAlias,
|
||||
":snd_private_key" := sndPrivateKey,
|
||||
":encrypt_key" := encryptKey,
|
||||
":sign_key" := signKey,
|
||||
":status" := status
|
||||
]
|
||||
|
||||
insertSndConnection_ :: DB.Connection -> SndQueue -> IO ()
|
||||
insertSndConnection_ dbConn SndQueue {server, sndId, connAlias} = do
|
||||
let port_ = serializePort_ $ port server
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO connections
|
||||
( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id,
|
||||
last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id,
|
||||
last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash)
|
||||
VALUES
|
||||
(:conn_alias, NULL, NULL, NULL,:snd_host,:snd_port,:snd_id,
|
||||
0, 0, 0, 0, x'', x'');
|
||||
|]
|
||||
[":conn_alias" := connAlias, ":snd_host" := host server, ":snd_port" := port_, ":snd_id" := sndId]
|
||||
|
||||
-- * getConn helpers
|
||||
|
||||
getConn_ :: DB.Connection -> ConnAlias -> IO (Either StoreError SomeConn)
|
||||
getConn_ dbConn connAlias = do
|
||||
rQ <- retrieveRcvQueueByConnAlias_ dbConn connAlias
|
||||
sQ <- retrieveSndQueueByConnAlias_ dbConn connAlias
|
||||
pure $ case (rQ, sQ) of
|
||||
(Just rcvQ, Just sndQ) -> Right $ SomeConn SCDuplex (DuplexConnection connAlias rcvQ sndQ)
|
||||
(Just rcvQ, Nothing) -> Right $ SomeConn SCRcv (RcvConnection connAlias rcvQ)
|
||||
(Nothing, Just sndQ) -> Right $ SomeConn SCSnd (SndConnection connAlias sndQ)
|
||||
_ -> Left SEConnNotFound
|
||||
|
||||
retrieveRcvQueueByConnAlias_ :: DB.Connection -> ConnAlias -> IO (Maybe RcvQueue)
|
||||
retrieveRcvQueueByConnAlias_ dbConn connAlias = do
|
||||
r <-
|
||||
DB.queryNamed
|
||||
dbConn
|
||||
[sql|
|
||||
SELECT
|
||||
s.key_hash, q.host, q.port, q.rcv_id, q.conn_alias, q.rcv_private_key,
|
||||
q.snd_id, q.snd_key, q.decrypt_key, q.verify_key, q.status
|
||||
FROM rcv_queues q
|
||||
INNER JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
WHERE q.conn_alias = :conn_alias;
|
||||
|]
|
||||
[":conn_alias" := connAlias]
|
||||
case r of
|
||||
[(keyHash, host, port, rcvId, cAlias, rcvPrivateKey, sndId, sndKey, decryptKey, verifyKey, status)] -> do
|
||||
let srv = SMPServer host (deserializePort_ port) keyHash
|
||||
return . Just $ RcvQueue srv rcvId cAlias rcvPrivateKey sndId sndKey decryptKey verifyKey status
|
||||
_ -> return Nothing
|
||||
|
||||
retrieveSndQueueByConnAlias_ :: DB.Connection -> ConnAlias -> IO (Maybe SndQueue)
|
||||
retrieveSndQueueByConnAlias_ dbConn connAlias = do
|
||||
r <-
|
||||
DB.queryNamed
|
||||
dbConn
|
||||
[sql|
|
||||
SELECT
|
||||
s.key_hash, q.host, q.port, q.snd_id, q.conn_alias,
|
||||
q.snd_private_key, q.encrypt_key, q.sign_key, q.status
|
||||
FROM snd_queues q
|
||||
INNER JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
WHERE q.conn_alias = :conn_alias;
|
||||
|]
|
||||
[":conn_alias" := connAlias]
|
||||
case r of
|
||||
[(keyHash, host, port, sndId, cAlias, sndPrivateKey, encryptKey, signKey, status)] -> do
|
||||
let srv = SMPServer host (deserializePort_ port) keyHash
|
||||
return . Just $ SndQueue srv sndId cAlias sndPrivateKey encryptKey signKey status
|
||||
_ -> return Nothing
|
||||
|
||||
-- * upgradeRcvConnToDuplex helpers
|
||||
|
||||
updateConnWithSndQueue_ :: DB.Connection -> ConnAlias -> SndQueue -> IO ()
|
||||
updateConnWithSndQueue_ dbConn connAlias SndQueue {server, sndId} = do
|
||||
let port_ = serializePort_ $ port server
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
UPDATE connections
|
||||
SET snd_host = :snd_host, snd_port = :snd_port, snd_id = :snd_id
|
||||
WHERE conn_alias = :conn_alias;
|
||||
|]
|
||||
[":snd_host" := host server, ":snd_port" := port_, ":snd_id" := sndId, ":conn_alias" := connAlias]
|
||||
|
||||
-- * upgradeSndConnToDuplex helpers
|
||||
|
||||
updateConnWithRcvQueue_ :: DB.Connection -> ConnAlias -> RcvQueue -> IO ()
|
||||
updateConnWithRcvQueue_ dbConn connAlias RcvQueue {server, rcvId} = do
|
||||
let port_ = serializePort_ $ port server
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
UPDATE connections
|
||||
SET rcv_host = :rcv_host, rcv_port = :rcv_port, rcv_id = :rcv_id
|
||||
WHERE conn_alias = :conn_alias;
|
||||
|]
|
||||
[":rcv_host" := host server, ":rcv_port" := port_, ":rcv_id" := rcvId, ":conn_alias" := connAlias]
|
||||
|
||||
-- * updateRcvIds helpers
|
||||
|
||||
retrieveLastIdsAndHashRcv_ :: DB.Connection -> ConnAlias -> IO (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
|
||||
retrieveLastIdsAndHashRcv_ dbConn connAlias = do
|
||||
[(lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash)] <-
|
||||
DB.queryNamed
|
||||
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_alias = :conn_alias;
|
||||
|]
|
||||
[":conn_alias" := connAlias]
|
||||
return (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash)
|
||||
|
||||
updateLastIdsRcv_ :: DB.Connection -> ConnAlias -> InternalId -> InternalRcvId -> IO ()
|
||||
updateLastIdsRcv_ dbConn connAlias newInternalId newInternalRcvId =
|
||||
DB.executeNamed
|
||||
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_alias = :conn_alias;
|
||||
|]
|
||||
[ ":last_internal_msg_id" := newInternalId,
|
||||
":last_internal_rcv_msg_id" := newInternalRcvId,
|
||||
":conn_alias" := connAlias
|
||||
]
|
||||
|
||||
-- * createRcvMsg helpers
|
||||
|
||||
insertRcvMsgBase_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO ()
|
||||
insertRcvMsgBase_ dbConn connAlias RcvMsgData {..} = do
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO messages
|
||||
( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body)
|
||||
VALUES
|
||||
(:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL,:body);
|
||||
|]
|
||||
[ ":conn_alias" := connAlias,
|
||||
":internal_id" := internalId,
|
||||
":internal_ts" := internalTs,
|
||||
":internal_rcv_id" := internalRcvId,
|
||||
":body" := decodeUtf8 msgBody
|
||||
]
|
||||
|
||||
insertRcvMsgDetails_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO ()
|
||||
insertRcvMsgDetails_ dbConn connAlias RcvMsgData {..} =
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO rcv_messages
|
||||
( conn_alias, internal_rcv_id, internal_id, external_snd_id, external_snd_ts,
|
||||
broker_id, broker_ts, rcv_status, ack_brocker_ts, ack_sender_ts,
|
||||
internal_hash, external_prev_snd_hash, integrity)
|
||||
VALUES
|
||||
(:conn_alias,:internal_rcv_id,:internal_id,:external_snd_id,:external_snd_ts,
|
||||
:broker_id,:broker_ts,:rcv_status, NULL, NULL,
|
||||
:internal_hash,:external_prev_snd_hash,:integrity);
|
||||
|]
|
||||
[ ":conn_alias" := connAlias,
|
||||
":internal_rcv_id" := internalRcvId,
|
||||
":internal_id" := internalId,
|
||||
":external_snd_id" := fst senderMeta,
|
||||
":external_snd_ts" := snd senderMeta,
|
||||
":broker_id" := fst brokerMeta,
|
||||
":broker_ts" := snd brokerMeta,
|
||||
":rcv_status" := Received,
|
||||
":internal_hash" := internalHash,
|
||||
":external_prev_snd_hash" := externalPrevSndHash,
|
||||
":integrity" := msgIntegrity
|
||||
]
|
||||
|
||||
updateHashRcv_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO ()
|
||||
updateHashRcv_ dbConn connAlias RcvMsgData {..} =
|
||||
DB.executeNamed
|
||||
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_external_snd_msg_id,
|
||||
last_rcv_msg_hash = :last_rcv_msg_hash
|
||||
WHERE conn_alias = :conn_alias
|
||||
AND last_internal_rcv_msg_id = :last_internal_rcv_msg_id;
|
||||
|]
|
||||
[ ":last_external_snd_msg_id" := fst senderMeta,
|
||||
":last_rcv_msg_hash" := internalHash,
|
||||
":conn_alias" := connAlias,
|
||||
":last_internal_rcv_msg_id" := internalRcvId
|
||||
]
|
||||
|
||||
-- * updateSndIds helpers
|
||||
|
||||
retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnAlias -> IO (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
retrieveLastIdsAndHashSnd_ dbConn connAlias = do
|
||||
[(lastInternalId, lastInternalSndId, lastSndHash)] <-
|
||||
DB.queryNamed
|
||||
dbConn
|
||||
[sql|
|
||||
SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash
|
||||
FROM connections
|
||||
WHERE conn_alias = :conn_alias;
|
||||
|]
|
||||
[":conn_alias" := connAlias]
|
||||
return (lastInternalId, lastInternalSndId, lastSndHash)
|
||||
|
||||
updateLastIdsSnd_ :: DB.Connection -> ConnAlias -> InternalId -> InternalSndId -> IO ()
|
||||
updateLastIdsSnd_ dbConn connAlias newInternalId newInternalSndId =
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
UPDATE connections
|
||||
SET last_internal_msg_id = :last_internal_msg_id,
|
||||
last_internal_snd_msg_id = :last_internal_snd_msg_id
|
||||
WHERE conn_alias = :conn_alias;
|
||||
|]
|
||||
[ ":last_internal_msg_id" := newInternalId,
|
||||
":last_internal_snd_msg_id" := newInternalSndId,
|
||||
":conn_alias" := connAlias
|
||||
]
|
||||
|
||||
-- * createSndMsg helpers
|
||||
|
||||
insertSndMsgBase_ :: DB.Connection -> ConnAlias -> SndMsgData -> IO ()
|
||||
insertSndMsgBase_ dbConn connAlias SndMsgData {..} = do
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO messages
|
||||
( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body)
|
||||
VALUES
|
||||
(:conn_alias,:internal_id,:internal_ts, NULL,:internal_snd_id,:body);
|
||||
|]
|
||||
[ ":conn_alias" := connAlias,
|
||||
":internal_id" := internalId,
|
||||
":internal_ts" := internalTs,
|
||||
":internal_snd_id" := internalSndId,
|
||||
":body" := decodeUtf8 msgBody
|
||||
]
|
||||
|
||||
insertSndMsgDetails_ :: DB.Connection -> ConnAlias -> SndMsgData -> IO ()
|
||||
insertSndMsgDetails_ dbConn connAlias SndMsgData {..} =
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
[sql|
|
||||
INSERT INTO snd_messages
|
||||
( conn_alias, internal_snd_id, internal_id, snd_status, sent_ts, delivered_ts, internal_hash)
|
||||
VALUES
|
||||
(:conn_alias,:internal_snd_id,:internal_id,:snd_status, NULL, NULL,:internal_hash);
|
||||
|]
|
||||
[ ":conn_alias" := connAlias,
|
||||
":internal_snd_id" := internalSndId,
|
||||
":internal_id" := internalId,
|
||||
":snd_status" := Created,
|
||||
":internal_hash" := internalHash
|
||||
]
|
||||
|
||||
updateHashSnd_ :: DB.Connection -> ConnAlias -> SndMsgData -> IO ()
|
||||
updateHashSnd_ dbConn connAlias SndMsgData {..} =
|
||||
DB.executeNamed
|
||||
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 = :last_snd_msg_hash
|
||||
WHERE conn_alias = :conn_alias
|
||||
AND last_internal_snd_msg_id = :last_internal_snd_msg_id;
|
||||
|]
|
||||
[ ":last_snd_msg_hash" := internalHash,
|
||||
":conn_alias" := connAlias,
|
||||
":last_internal_snd_msg_id" := internalSndId
|
||||
]
|
||||
@@ -0,0 +1,167 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Schema (createSchema) where
|
||||
|
||||
import Database.SQLite.Simple (Connection, Query, execute_)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
createSchema :: Connection -> IO ()
|
||||
createSchema conn =
|
||||
mapM_
|
||||
(execute_ conn)
|
||||
[ servers,
|
||||
rcvQueues,
|
||||
sndQueues,
|
||||
connections,
|
||||
messages,
|
||||
rcvMessages,
|
||||
sndMessages
|
||||
]
|
||||
|
||||
-- port is either a port number or a service name, see Network.Socket.Info.ServiceName
|
||||
servers :: Query
|
||||
servers =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS servers(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BLOB,
|
||||
PRIMARY KEY (host, port)
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
|
||||
rcvQueues :: Query
|
||||
rcvQueues =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS rcv_queues(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
rcv_id BLOB NOT NULL,
|
||||
conn_alias BLOB NOT NULL,
|
||||
rcv_private_key BLOB NOT NULL,
|
||||
snd_id BLOB,
|
||||
snd_key BLOB,
|
||||
decrypt_key BLOB NOT NULL,
|
||||
verify_key BLOB,
|
||||
status TEXT NOT NULL,
|
||||
PRIMARY KEY (host, port, rcv_id),
|
||||
FOREIGN KEY (host, port) REFERENCES servers (host, port),
|
||||
FOREIGN KEY (conn_alias)
|
||||
REFERENCES connections (conn_alias)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
UNIQUE (host, port, snd_id)
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
|
||||
sndQueues :: Query
|
||||
sndQueues =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS snd_queues(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
snd_id BLOB NOT NULL,
|
||||
conn_alias BLOB NOT NULL,
|
||||
snd_private_key BLOB NOT NULL,
|
||||
encrypt_key BLOB NOT NULL,
|
||||
sign_key BLOB NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
PRIMARY KEY (host, port, snd_id),
|
||||
FOREIGN KEY (host, port) REFERENCES servers (host, port),
|
||||
FOREIGN KEY (conn_alias)
|
||||
REFERENCES connections (conn_alias)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
|
||||
connections :: Query
|
||||
connections =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS connections(
|
||||
conn_alias BLOB NOT NULL,
|
||||
rcv_host TEXT,
|
||||
rcv_port TEXT,
|
||||
rcv_id BLOB,
|
||||
snd_host TEXT,
|
||||
snd_port TEXT,
|
||||
snd_id BLOB,
|
||||
last_internal_msg_id INTEGER NOT NULL,
|
||||
last_internal_rcv_msg_id INTEGER NOT NULL,
|
||||
last_internal_snd_msg_id INTEGER NOT NULL,
|
||||
last_external_snd_msg_id INTEGER NOT NULL,
|
||||
last_rcv_msg_hash BLOB NOT NULL,
|
||||
last_snd_msg_hash BLOB NOT NULL,
|
||||
PRIMARY KEY (conn_alias),
|
||||
FOREIGN KEY (rcv_host, rcv_port, rcv_id) REFERENCES rcv_queues (host, port, rcv_id),
|
||||
FOREIGN KEY (snd_host, snd_port, snd_id) REFERENCES snd_queues (host, port, snd_id)
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
|
||||
messages :: Query
|
||||
messages =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS messages(
|
||||
conn_alias BLOB NOT NULL,
|
||||
internal_id INTEGER NOT NULL,
|
||||
internal_ts TEXT NOT NULL,
|
||||
internal_rcv_id INTEGER,
|
||||
internal_snd_id INTEGER,
|
||||
body TEXT NOT NULL,
|
||||
PRIMARY KEY (conn_alias, internal_id),
|
||||
FOREIGN KEY (conn_alias)
|
||||
REFERENCES connections (conn_alias)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (conn_alias, internal_rcv_id)
|
||||
REFERENCES rcv_messages (conn_alias, internal_rcv_id)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
FOREIGN KEY (conn_alias, internal_snd_id)
|
||||
REFERENCES snd_messages (conn_alias, internal_snd_id)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
|
||||
rcvMessages :: Query
|
||||
rcvMessages =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS rcv_messages(
|
||||
conn_alias BLOB NOT NULL,
|
||||
internal_rcv_id INTEGER NOT NULL,
|
||||
internal_id INTEGER NOT NULL,
|
||||
external_snd_id INTEGER NOT NULL,
|
||||
external_snd_ts TEXT NOT NULL,
|
||||
broker_id BLOB NOT NULL,
|
||||
broker_ts TEXT NOT NULL,
|
||||
rcv_status TEXT NOT NULL,
|
||||
ack_brocker_ts TEXT,
|
||||
ack_sender_ts TEXT,
|
||||
internal_hash BLOB NOT NULL,
|
||||
external_prev_snd_hash BLOB NOT NULL,
|
||||
integrity BLOB NOT NULL,
|
||||
PRIMARY KEY (conn_alias, internal_rcv_id),
|
||||
FOREIGN KEY (conn_alias, internal_id)
|
||||
REFERENCES messages (conn_alias, internal_id)
|
||||
ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
|
||||
sndMessages :: Query
|
||||
sndMessages =
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS snd_messages(
|
||||
conn_alias BLOB NOT NULL,
|
||||
internal_snd_id INTEGER NOT NULL,
|
||||
internal_id INTEGER NOT NULL,
|
||||
snd_status TEXT NOT NULL,
|
||||
sent_ts TEXT,
|
||||
delivered_ts TEXT,
|
||||
internal_hash BLOB NOT NULL,
|
||||
PRIMARY KEY (conn_alias, internal_snd_id),
|
||||
FOREIGN KEY (conn_alias, internal_id)
|
||||
REFERENCES messages (conn_alias, internal_id)
|
||||
ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
@@ -0,0 +1,461 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Transmission where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.ISO8601
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable ()
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Network.Socket
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol
|
||||
( CorrId (..),
|
||||
Encoded,
|
||||
ErrorType,
|
||||
MsgBody,
|
||||
MsgId,
|
||||
SenderPublicKey,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util
|
||||
import System.IO
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
import Text.Read
|
||||
import UnliftIO.Exception
|
||||
|
||||
type ARawTransmission = (ByteString, ByteString, ByteString)
|
||||
|
||||
type ATransmission p = (CorrId, ConnAlias, ACommand p)
|
||||
|
||||
type ATransmissionOrError p = (CorrId, ConnAlias, Either AgentErrorType (ACommand p))
|
||||
|
||||
data AParty = Agent | Client
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SAParty :: AParty -> Type where
|
||||
SAgent :: SAParty Agent
|
||||
SClient :: SAParty Client
|
||||
|
||||
deriving instance Show (SAParty p)
|
||||
|
||||
deriving instance Eq (SAParty p)
|
||||
|
||||
instance TestEquality SAParty where
|
||||
testEquality SAgent SAgent = Just Refl
|
||||
testEquality SClient SClient = Just Refl
|
||||
testEquality _ _ = Nothing
|
||||
|
||||
data ACmd = forall p. ACmd (SAParty p) (ACommand p)
|
||||
|
||||
deriving instance Show ACmd
|
||||
|
||||
data ACommand (p :: AParty) where
|
||||
NEW :: SMPServer -> ACommand Client -- response INV
|
||||
INV :: SMPQueueInfo -> ACommand Agent
|
||||
JOIN :: SMPQueueInfo -> ReplyMode -> ACommand Client -- response OK
|
||||
CON :: ACommand Agent -- notification that connection is established
|
||||
-- TODO currently it automatically allows whoever sends the confirmation
|
||||
-- CONF :: OtherPartyId -> ACommand Agent
|
||||
-- LET :: OtherPartyId -> ACommand Client
|
||||
SUB :: ACommand Client
|
||||
SUBALL :: ACommand Client -- TODO should be moved to chat protocol - hack for subscribing to all
|
||||
END :: ACommand Agent
|
||||
-- QST :: QueueDirection -> ACommand Client
|
||||
-- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent
|
||||
SEND :: MsgBody -> ACommand Client
|
||||
SENT :: AgentMsgId -> ACommand Agent
|
||||
MSG ::
|
||||
{ recipientMeta :: (AgentMsgId, UTCTime),
|
||||
brokerMeta :: (MsgId, UTCTime),
|
||||
senderMeta :: (AgentMsgId, UTCTime),
|
||||
msgIntegrity :: MsgIntegrity,
|
||||
msgBody :: MsgBody
|
||||
} ->
|
||||
ACommand Agent
|
||||
-- ACK :: AgentMsgId -> ACommand Client
|
||||
-- RCVD :: AgentMsgId -> ACommand Agent
|
||||
OFF :: ACommand Client
|
||||
DEL :: ACommand Client
|
||||
OK :: ACommand Agent
|
||||
ERR :: AgentErrorType -> ACommand Agent
|
||||
|
||||
deriving instance Eq (ACommand p)
|
||||
|
||||
deriving instance Show (ACommand p)
|
||||
|
||||
type Message = ByteString
|
||||
|
||||
data SMPMessage
|
||||
= SMPConfirmation SenderPublicKey
|
||||
| SMPMessage
|
||||
{ senderMsgId :: AgentMsgId,
|
||||
senderTimestamp :: SenderTimestamp,
|
||||
previousMsgHash :: ByteString,
|
||||
agentMessage :: AMessage
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data AMessage where
|
||||
HELLO :: VerificationKey -> AckMode -> AMessage
|
||||
REPLY :: SMPQueueInfo -> AMessage
|
||||
A_MSG :: MsgBody -> AMessage
|
||||
deriving (Show)
|
||||
|
||||
parseSMPMessage :: ByteString -> Either AgentErrorType SMPMessage
|
||||
parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE
|
||||
where
|
||||
smpMessageP :: Parser SMPMessage
|
||||
smpMessageP =
|
||||
smpConfirmationP <* A.endOfLine
|
||||
<|> A.endOfLine *> smpClientMessageP
|
||||
|
||||
smpConfirmationP :: Parser SMPMessage
|
||||
smpConfirmationP = SMPConfirmation <$> ("KEY " *> C.pubKeyP <* A.endOfLine)
|
||||
|
||||
smpClientMessageP :: Parser SMPMessage
|
||||
smpClientMessageP =
|
||||
SMPMessage
|
||||
<$> A.decimal <* A.space
|
||||
<*> tsISO8601P <* A.space
|
||||
-- TODO previous message hash should become mandatory when we support HELLO and REPLY
|
||||
-- (for HELLO it would be the hash of SMPConfirmation)
|
||||
<*> (base64P <|> pure "") <* A.endOfLine
|
||||
<*> agentMessageP
|
||||
|
||||
serializeSMPMessage :: SMPMessage -> ByteString
|
||||
serializeSMPMessage = \case
|
||||
SMPConfirmation sKey -> smpMessage ("KEY " <> C.serializePubKey sKey) "" ""
|
||||
SMPMessage {senderMsgId, senderTimestamp, previousMsgHash, agentMessage} ->
|
||||
let header = messageHeader senderMsgId senderTimestamp previousMsgHash
|
||||
body = serializeAgentMessage agentMessage
|
||||
in smpMessage "" header body
|
||||
where
|
||||
messageHeader msgId ts prevMsgHash =
|
||||
B.unwords [bshow msgId, B.pack $ formatISO8601Millis ts, encode prevMsgHash]
|
||||
smpMessage smpHeader aHeader aBody = B.intercalate "\n" [smpHeader, aHeader, aBody, ""]
|
||||
|
||||
agentMessageP :: Parser AMessage
|
||||
agentMessageP =
|
||||
"HELLO " *> hello
|
||||
<|> "REPLY " *> reply
|
||||
<|> "MSG " *> a_msg
|
||||
where
|
||||
hello = HELLO <$> C.pubKeyP <*> ackMode
|
||||
reply = REPLY <$> smpQueueInfoP
|
||||
a_msg = do
|
||||
size :: Int <- A.decimal <* A.endOfLine
|
||||
A_MSG <$> A.take size <* A.endOfLine
|
||||
ackMode = " NO_ACK" $> AckMode Off <|> pure (AckMode On)
|
||||
|
||||
smpQueueInfoP :: Parser SMPQueueInfo
|
||||
smpQueueInfoP =
|
||||
"smp::" *> (SMPQueueInfo <$> smpServerP <* "::" <*> base64P <* "::" <*> C.pubKeyP)
|
||||
|
||||
smpServerP :: Parser SMPServer
|
||||
smpServerP = SMPServer <$> server <*> optional port <*> optional kHash
|
||||
where
|
||||
server = B.unpack <$> A.takeTill (A.inClass ":# ")
|
||||
port = A.char ':' *> (B.unpack <$> A.takeWhile1 A.isDigit)
|
||||
kHash = C.KeyHash <$> (A.char '#' *> base64P)
|
||||
|
||||
parseAgentMessage :: ByteString -> Either AgentErrorType AMessage
|
||||
parseAgentMessage = parse agentMessageP $ AGENT A_MESSAGE
|
||||
|
||||
serializeAgentMessage :: AMessage -> ByteString
|
||||
serializeAgentMessage = \case
|
||||
HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else ""
|
||||
REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo
|
||||
A_MSG body -> "MSG " <> serializeMsg body <> "\n"
|
||||
|
||||
serializeSmpQueueInfo :: SMPQueueInfo -> ByteString
|
||||
serializeSmpQueueInfo (SMPQueueInfo srv qId ek) =
|
||||
B.intercalate "::" ["smp", serializeServer srv, encode qId, C.serializePubKey ek]
|
||||
|
||||
serializeServer :: SMPServer -> ByteString
|
||||
serializeServer SMPServer {host, port, keyHash} =
|
||||
B.pack $ host <> maybe "" (':' :) port <> maybe "" (('#' :) . B.unpack . encode . C.unKeyHash) keyHash
|
||||
|
||||
data SMPServer = SMPServer
|
||||
{ host :: HostName,
|
||||
port :: Maybe ServiceName,
|
||||
keyHash :: Maybe C.KeyHash
|
||||
}
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
type ConnAlias = ByteString
|
||||
|
||||
type OtherPartyId = Encoded
|
||||
|
||||
data Mode = On | Off deriving (Eq, Show, Read)
|
||||
|
||||
newtype AckMode = AckMode Mode deriving (Eq, Show)
|
||||
|
||||
data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId EncryptionKey
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ReplyMode = ReplyOff | ReplyOn | ReplyVia SMPServer deriving (Eq, Show)
|
||||
|
||||
type EncryptionKey = C.PublicKey
|
||||
|
||||
type DecryptionKey = C.SafePrivateKey
|
||||
|
||||
type SignatureKey = C.SafePrivateKey
|
||||
|
||||
type VerificationKey = C.PublicKey
|
||||
|
||||
data QueueDirection = SND | RCV deriving (Show)
|
||||
|
||||
data QueueStatus = New | Confirmed | Secured | Active | Disabled
|
||||
deriving (Eq, Show, Read)
|
||||
|
||||
type AgentMsgId = Int64
|
||||
|
||||
type SenderTimestamp = UTCTime
|
||||
|
||||
data MsgIntegrity = MsgOk | MsgError MsgErrorType
|
||||
deriving (Eq, Show)
|
||||
|
||||
data MsgErrorType = MsgSkipped AgentMsgId AgentMsgId | MsgBadId AgentMsgId | MsgBadHash | MsgDuplicate
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | error type used in errors sent to agent clients
|
||||
data AgentErrorType
|
||||
= CMD CommandErrorType -- command errors
|
||||
| CONN ConnectionErrorType -- connection state errors
|
||||
| SMP ErrorType -- SMP protocol errors forwarded to agent clients
|
||||
| BROKER BrokerErrorType -- SMP server errors
|
||||
| AGENT SMPAgentError -- errors of other agents
|
||||
| INTERNAL String -- agent implementation errors
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
data CommandErrorType
|
||||
= PROHIBITED -- command is prohibited
|
||||
| SYNTAX -- command syntax is invalid
|
||||
| NO_CONN -- connection alias is required with this command
|
||||
| SIZE -- message size is not correct (no terminating space)
|
||||
| LARGE -- message does not fit SMP block
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
data ConnectionErrorType
|
||||
= UNKNOWN -- connection alias not in database
|
||||
| DUPLICATE -- connection alias already exists
|
||||
| SIMPLEX -- connection is simplex, but operation requires another queue
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
data BrokerErrorType
|
||||
= RESPONSE ErrorType -- invalid server response (failed to parse)
|
||||
| UNEXPECTED -- unexpected response
|
||||
| NETWORK -- network error
|
||||
| TRANSPORT TransportError -- handshake or other transport error
|
||||
| TIMEOUT -- command response timeout
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
data SMPAgentError
|
||||
= A_MESSAGE -- possibly should include bytestring that failed to parse
|
||||
| A_PROHIBITED -- possibly should include the prohibited SMP/agent message
|
||||
| A_ENCRYPTION -- cannot RSA/AES-decrypt or parse decrypted header
|
||||
| A_SIGNATURE -- invalid RSA signature
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance Arbitrary AgentErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU
|
||||
|
||||
commandP :: Parser ACmd
|
||||
commandP =
|
||||
"NEW " *> newCmd
|
||||
<|> "INV " *> invResp
|
||||
<|> "JOIN " *> joinCmd
|
||||
<|> "SUB" $> ACmd SClient SUB
|
||||
<|> "SUBALL" $> ACmd SClient SUBALL -- TODO remove - hack for subscribing to all
|
||||
<|> "END" $> ACmd SAgent END
|
||||
<|> "SEND " *> sendCmd
|
||||
<|> "SENT " *> sentResp
|
||||
<|> "MSG " *> message
|
||||
<|> "OFF" $> ACmd SClient OFF
|
||||
<|> "DEL" $> ACmd SClient DEL
|
||||
<|> "ERR " *> agentError
|
||||
<|> "CON" $> ACmd SAgent CON
|
||||
<|> "OK" $> ACmd SAgent OK
|
||||
where
|
||||
newCmd = ACmd SClient . NEW <$> smpServerP
|
||||
invResp = ACmd SAgent . INV <$> smpQueueInfoP
|
||||
joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <*> replyMode)
|
||||
sendCmd = ACmd SClient . SEND <$> A.takeByteString
|
||||
sentResp = ACmd SAgent . SENT <$> A.decimal
|
||||
message = do
|
||||
msgIntegrity <- msgIntegrityP <* A.space
|
||||
recipientMeta <- "R=" *> partyMeta A.decimal
|
||||
brokerMeta <- "B=" *> partyMeta base64P
|
||||
senderMeta <- "S=" *> partyMeta A.decimal
|
||||
msgBody <- A.takeByteString
|
||||
return $ ACmd SAgent MSG {recipientMeta, brokerMeta, senderMeta, msgIntegrity, msgBody}
|
||||
replyMode =
|
||||
" NO_REPLY" $> ReplyOff
|
||||
<|> A.space *> (ReplyVia <$> smpServerP)
|
||||
<|> pure ReplyOn
|
||||
partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space
|
||||
agentError = ACmd SAgent . ERR <$> agentErrorTypeP
|
||||
|
||||
msgIntegrityP :: Parser MsgIntegrity
|
||||
msgIntegrityP = "OK" $> MsgOk <|> "ERR " *> (MsgError <$> msgErrorType)
|
||||
where
|
||||
msgErrorType =
|
||||
"ID " *> (MsgBadId <$> A.decimal)
|
||||
<|> "IDS " *> (MsgSkipped <$> A.decimal <* A.space <*> A.decimal)
|
||||
<|> "HASH" $> MsgBadHash
|
||||
<|> "DUPLICATE" $> MsgDuplicate
|
||||
|
||||
parseCommand :: ByteString -> Either AgentErrorType ACmd
|
||||
parseCommand = parse commandP $ CMD SYNTAX
|
||||
|
||||
serializeCommand :: ACommand p -> ByteString
|
||||
serializeCommand = \case
|
||||
NEW srv -> "NEW " <> serializeServer srv
|
||||
INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo
|
||||
JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode
|
||||
SUB -> "SUB"
|
||||
SUBALL -> "SUBALL" -- TODO remove - hack for subscribing to all
|
||||
END -> "END"
|
||||
SEND msgBody -> "SEND " <> serializeMsg msgBody
|
||||
SENT mId -> "SENT " <> bshow mId
|
||||
MSG {recipientMeta = (rmId, rTs), brokerMeta = (bmId, bTs), senderMeta = (smId, sTs), msgIntegrity, msgBody} ->
|
||||
B.unwords
|
||||
[ "MSG",
|
||||
serializeMsgIntegrity msgIntegrity,
|
||||
"R=" <> bshow rmId <> "," <> showTs rTs,
|
||||
"B=" <> encode bmId <> "," <> showTs bTs,
|
||||
"S=" <> bshow smId <> "," <> showTs sTs,
|
||||
serializeMsg msgBody
|
||||
]
|
||||
OFF -> "OFF"
|
||||
DEL -> "DEL"
|
||||
CON -> "CON"
|
||||
ERR e -> "ERR " <> serializeAgentError e
|
||||
OK -> "OK"
|
||||
where
|
||||
replyMode :: ReplyMode -> ByteString
|
||||
replyMode = \case
|
||||
ReplyOff -> " NO_REPLY"
|
||||
ReplyVia srv -> " " <> serializeServer srv
|
||||
ReplyOn -> ""
|
||||
showTs :: UTCTime -> ByteString
|
||||
showTs = B.pack . formatISO8601Millis
|
||||
|
||||
serializeMsgIntegrity :: MsgIntegrity -> ByteString
|
||||
serializeMsgIntegrity = \case
|
||||
MsgOk -> "OK"
|
||||
MsgError e ->
|
||||
"ERR " <> case e of
|
||||
MsgSkipped fromMsgId toMsgId ->
|
||||
B.unwords ["NO_ID", bshow fromMsgId, bshow toMsgId]
|
||||
MsgBadId aMsgId -> "ID " <> bshow aMsgId
|
||||
MsgBadHash -> "HASH"
|
||||
MsgDuplicate -> "DUPLICATE"
|
||||
|
||||
agentErrorTypeP :: Parser AgentErrorType
|
||||
agentErrorTypeP =
|
||||
"SMP " *> (SMP <$> SMP.errorTypeP)
|
||||
<|> "BROKER RESPONSE " *> (BROKER . RESPONSE <$> SMP.errorTypeP)
|
||||
<|> "BROKER TRANSPORT " *> (BROKER . TRANSPORT <$> transportErrorP)
|
||||
<|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString)
|
||||
<|> parseRead2
|
||||
|
||||
serializeAgentError :: AgentErrorType -> ByteString
|
||||
serializeAgentError = \case
|
||||
SMP e -> "SMP " <> SMP.serializeErrorType e
|
||||
BROKER (RESPONSE e) -> "BROKER RESPONSE " <> SMP.serializeErrorType e
|
||||
BROKER (TRANSPORT e) -> "BROKER TRANSPORT " <> serializeTransportError e
|
||||
e -> bshow e
|
||||
|
||||
serializeMsg :: ByteString -> ByteString
|
||||
serializeMsg body = bshow (B.length body) <> "\n" <> body
|
||||
|
||||
tPutRaw :: Handle -> ARawTransmission -> IO ()
|
||||
tPutRaw h (corrId, connAlias, command) = do
|
||||
putLn h corrId
|
||||
putLn h connAlias
|
||||
putLn h command
|
||||
|
||||
tGetRaw :: Handle -> IO ARawTransmission
|
||||
tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h
|
||||
|
||||
tPut :: MonadIO m => Handle -> ATransmission p -> m ()
|
||||
tPut h (CorrId corrId, connAlias, command) =
|
||||
liftIO $ tPutRaw h (corrId, connAlias, serializeCommand command)
|
||||
|
||||
-- | get client and agent transmissions
|
||||
tGet :: forall m p. MonadIO m => SAParty p -> Handle -> m (ATransmissionOrError p)
|
||||
tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
where
|
||||
tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p)
|
||||
tParseLoadBody t@(corrId, connAlias, command) = do
|
||||
let cmd = parseCommand command >>= fromParty >>= tConnAlias t
|
||||
fullCmd <- either (return . Left) cmdWithMsgBody cmd
|
||||
return (CorrId corrId, connAlias, fullCmd)
|
||||
|
||||
fromParty :: ACmd -> Either AgentErrorType (ACommand p)
|
||||
fromParty (ACmd (p :: p1) cmd) = case testEquality party p of
|
||||
Just Refl -> Right cmd
|
||||
_ -> Left $ CMD PROHIBITED
|
||||
|
||||
tConnAlias :: ARawTransmission -> ACommand p -> Either AgentErrorType (ACommand p)
|
||||
tConnAlias (_, connAlias, _) cmd = case cmd of
|
||||
-- NEW and JOIN have optional connAlias
|
||||
NEW _ -> Right cmd
|
||||
JOIN _ _ -> Right cmd
|
||||
-- ERROR response does not always have connAlias
|
||||
ERR _ -> Right cmd
|
||||
-- other responses must have connAlias
|
||||
_
|
||||
| B.null connAlias -> Left $ CMD NO_CONN
|
||||
| otherwise -> Right cmd
|
||||
|
||||
cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p))
|
||||
cmdWithMsgBody = \case
|
||||
SEND body -> SEND <$$> getMsgBody body
|
||||
MSG agentMsgId srvTS agentTS integrity body -> MSG agentMsgId srvTS agentTS integrity <$$> getMsgBody body
|
||||
cmd -> return $ Right cmd
|
||||
|
||||
-- TODO refactor with server
|
||||
getMsgBody :: MsgBody -> m (Either AgentErrorType MsgBody)
|
||||
getMsgBody msgBody =
|
||||
case B.unpack msgBody of
|
||||
':' : body -> return . Right $ B.pack body
|
||||
str -> case readMaybe str :: Maybe Int of
|
||||
Just size -> liftIO $ do
|
||||
body <- B.hGet h size
|
||||
s <- getLn h
|
||||
return $ if B.null s then Right body else Left $ CMD SIZE
|
||||
Nothing -> return . Left $ CMD SYNTAX
|
||||
@@ -0,0 +1,269 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Client
|
||||
( SMPClient (blockSize),
|
||||
getSMPClient,
|
||||
closeSMPClient,
|
||||
createSMPQueue,
|
||||
subscribeSMPQueue,
|
||||
secureSMPQueue,
|
||||
sendSMPMessage,
|
||||
ackSMPMessage,
|
||||
sendSMPCommand,
|
||||
suspendSMPQueue,
|
||||
deleteSMPQueue,
|
||||
SMPClientError (..),
|
||||
SMPClientConfig (..),
|
||||
smpDefaultConfig,
|
||||
SMPServerTransmission,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Trans.Class
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe
|
||||
import Network.Socket (ServiceName)
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.Transmission (SMPServer (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (bshow, liftError, raceAny_)
|
||||
import System.IO
|
||||
import System.Timeout
|
||||
|
||||
data SMPClient = SMPClient
|
||||
{ action :: Async (),
|
||||
connected :: TVar Bool,
|
||||
smpServer :: SMPServer,
|
||||
tcpTimeout :: Int,
|
||||
clientCorrId :: TVar Natural,
|
||||
sentCommands :: TVar (Map CorrId Request),
|
||||
sndQ :: TBQueue SignedRawTransmission,
|
||||
rcvQ :: TBQueue SignedTransmissionOrError,
|
||||
msgQ :: TBQueue SMPServerTransmission,
|
||||
blockSize :: Int
|
||||
}
|
||||
|
||||
type SMPServerTransmission = (SMPServer, RecipientId, Command 'Broker)
|
||||
|
||||
data SMPClientConfig = SMPClientConfig
|
||||
{ qSize :: Natural,
|
||||
defaultPort :: ServiceName,
|
||||
tcpTimeout :: Int,
|
||||
smpPing :: Int,
|
||||
smpCommandSize :: Int
|
||||
}
|
||||
|
||||
smpDefaultConfig :: SMPClientConfig
|
||||
smpDefaultConfig =
|
||||
SMPClientConfig
|
||||
{ qSize = 16,
|
||||
defaultPort = "5223",
|
||||
tcpTimeout = 4_000_000,
|
||||
smpPing = 30_000_000,
|
||||
smpCommandSize = 256
|
||||
}
|
||||
|
||||
data Request = Request
|
||||
{ queueId :: QueueId,
|
||||
responseVar :: TMVar Response
|
||||
}
|
||||
|
||||
type Response = Either SMPClientError Cmd
|
||||
|
||||
getSMPClient :: SMPServer -> SMPClientConfig -> TBQueue SMPServerTransmission -> IO () -> IO (Either SMPClientError SMPClient)
|
||||
getSMPClient
|
||||
smpServer@SMPServer {host, port, keyHash}
|
||||
SMPClientConfig {qSize, defaultPort, tcpTimeout, smpPing}
|
||||
msgQ
|
||||
disconnected = do
|
||||
c <- atomically mkSMPClient
|
||||
thVar <- newEmptyTMVarIO
|
||||
action <-
|
||||
async $
|
||||
runTCPClient host (fromMaybe defaultPort port) (client c thVar)
|
||||
`finally` atomically (putTMVar thVar $ Left SMPNetworkError)
|
||||
tHandle <- tcpTimeout `timeout` atomically (takeTMVar thVar)
|
||||
pure $ case tHandle of
|
||||
Just (Right THandle {blockSize}) -> Right c {action, blockSize}
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left SMPNetworkError
|
||||
where
|
||||
mkSMPClient :: STM SMPClient
|
||||
mkSMPClient = do
|
||||
connected <- newTVar False
|
||||
clientCorrId <- newTVar 0
|
||||
sentCommands <- newTVar M.empty
|
||||
sndQ <- newTBQueue qSize
|
||||
rcvQ <- newTBQueue qSize
|
||||
return
|
||||
SMPClient
|
||||
{ action = undefined,
|
||||
blockSize = undefined,
|
||||
connected,
|
||||
smpServer,
|
||||
tcpTimeout,
|
||||
clientCorrId,
|
||||
sentCommands,
|
||||
sndQ,
|
||||
rcvQ,
|
||||
msgQ
|
||||
}
|
||||
|
||||
client :: SMPClient -> TMVar (Either SMPClientError THandle) -> Handle -> IO ()
|
||||
client c thVar h =
|
||||
runExceptT (clientHandshake h keyHash) >>= \case
|
||||
Right th -> clientTransport c thVar th
|
||||
Left e -> atomically . putTMVar thVar . Left $ SMPTransportError e
|
||||
|
||||
clientTransport :: SMPClient -> TMVar (Either SMPClientError THandle) -> THandle -> IO ()
|
||||
clientTransport c thVar th = do
|
||||
atomically $ do
|
||||
writeTVar (connected c) True
|
||||
putTMVar thVar $ Right th
|
||||
raceAny_ [send c th, process c, receive c th, ping c]
|
||||
`finally` disconnected
|
||||
|
||||
send :: SMPClient -> THandle -> IO ()
|
||||
send SMPClient {sndQ} h = forever $ atomically (readTBQueue sndQ) >>= tPut h
|
||||
|
||||
receive :: SMPClient -> THandle -> IO ()
|
||||
receive SMPClient {rcvQ} h = forever $ tGet fromServer h >>= atomically . writeTBQueue rcvQ
|
||||
|
||||
ping :: SMPClient -> IO ()
|
||||
ping c = forever $ do
|
||||
threadDelay smpPing
|
||||
runExceptT $ sendSMPCommand c Nothing "" (Cmd SSender PING)
|
||||
|
||||
process :: SMPClient -> IO ()
|
||||
process SMPClient {rcvQ, sentCommands} = forever $ do
|
||||
(_, (corrId, qId, respOrErr)) <- atomically $ readTBQueue rcvQ
|
||||
cs <- readTVarIO sentCommands
|
||||
case M.lookup corrId cs of
|
||||
Nothing -> do
|
||||
case respOrErr of
|
||||
Right (Cmd SBroker cmd) -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd)
|
||||
-- TODO send everything else to errQ and log in agent
|
||||
_ -> return ()
|
||||
Just Request {queueId, responseVar} -> atomically $ do
|
||||
modifyTVar sentCommands $ M.delete corrId
|
||||
putTMVar responseVar $
|
||||
if queueId == qId
|
||||
then case respOrErr of
|
||||
Left e -> Left $ SMPResponseError e
|
||||
Right (Cmd _ (ERR e)) -> Left $ SMPServerError e
|
||||
Right r -> Right r
|
||||
else Left SMPUnexpectedResponse
|
||||
|
||||
closeSMPClient :: SMPClient -> IO ()
|
||||
closeSMPClient = uninterruptibleCancel . action
|
||||
|
||||
data SMPClientError
|
||||
= SMPServerError ErrorType
|
||||
| SMPResponseError ErrorType
|
||||
| SMPUnexpectedResponse
|
||||
| SMPResponseTimeout
|
||||
| SMPNetworkError
|
||||
| SMPTransportError TransportError
|
||||
| SMPSignatureError C.CryptoError
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
createSMPQueue ::
|
||||
SMPClient ->
|
||||
RecipientPrivateKey ->
|
||||
RecipientPublicKey ->
|
||||
ExceptT SMPClientError IO (RecipientId, SenderId)
|
||||
createSMPQueue c rpKey rKey =
|
||||
-- TODO add signing this request too - requires changes in the server
|
||||
sendSMPCommand c (Just rpKey) "" (Cmd SRecipient $ NEW rKey) >>= \case
|
||||
Cmd _ (IDS rId sId) -> return (rId, sId)
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
subscribeSMPQueue :: SMPClient -> RecipientPrivateKey -> RecipientId -> ExceptT SMPClientError IO ()
|
||||
subscribeSMPQueue c@SMPClient {smpServer, msgQ} rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId (Cmd SRecipient SUB) >>= \case
|
||||
Cmd _ OK -> return ()
|
||||
Cmd _ cmd@MSG {} ->
|
||||
lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
secureSMPQueue :: SMPClient -> RecipientPrivateKey -> RecipientId -> SenderPublicKey -> ExceptT SMPClientError IO ()
|
||||
secureSMPQueue c rpKey rId senderKey = okSMPCommand (Cmd SRecipient $ KEY senderKey) c rpKey rId
|
||||
|
||||
sendSMPMessage :: SMPClient -> Maybe SenderPrivateKey -> SenderId -> MsgBody -> ExceptT SMPClientError IO ()
|
||||
sendSMPMessage c spKey sId msg =
|
||||
sendSMPCommand c spKey sId (Cmd SSender $ SEND msg) >>= \case
|
||||
Cmd _ OK -> return ()
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
ackSMPMessage :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
ackSMPMessage c@SMPClient {smpServer, msgQ} rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId (Cmd SRecipient ACK) >>= \case
|
||||
Cmd _ OK -> return ()
|
||||
Cmd _ cmd@MSG {} ->
|
||||
lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
suspendSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
suspendSMPQueue = okSMPCommand $ Cmd SRecipient OFF
|
||||
|
||||
deleteSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
deleteSMPQueue = okSMPCommand $ Cmd SRecipient DEL
|
||||
|
||||
okSMPCommand :: Cmd -> SMPClient -> C.SafePrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
okSMPCommand cmd c pKey qId =
|
||||
sendSMPCommand c (Just pKey) qId cmd >>= \case
|
||||
Cmd _ OK -> return ()
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
sendSMPCommand :: SMPClient -> Maybe C.SafePrivateKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd
|
||||
sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, tcpTimeout} pKey qId cmd = do
|
||||
corrId <- lift_ getNextCorrId
|
||||
t <- signTransmission $ serializeTransmission (corrId, qId, cmd)
|
||||
ExceptT $ sendRecv corrId t
|
||||
where
|
||||
lift_ :: STM a -> ExceptT SMPClientError IO a
|
||||
lift_ action = ExceptT $ Right <$> atomically action
|
||||
|
||||
getNextCorrId :: STM CorrId
|
||||
getNextCorrId = do
|
||||
i <- (+ 1) <$> readTVar clientCorrId
|
||||
writeTVar clientCorrId i
|
||||
return . CorrId $ bshow i
|
||||
|
||||
signTransmission :: ByteString -> ExceptT SMPClientError IO SignedRawTransmission
|
||||
signTransmission t = case pKey of
|
||||
Nothing -> return ("", t)
|
||||
Just pk -> do
|
||||
sig <- liftError SMPSignatureError $ C.sign pk t
|
||||
return (sig, t)
|
||||
|
||||
-- two separate "atomically" needed to avoid blocking
|
||||
sendRecv :: CorrId -> SignedRawTransmission -> IO Response
|
||||
sendRecv corrId t = atomically (send corrId t) >>= withTimeout . atomically . takeTMVar
|
||||
where
|
||||
withTimeout a = fromMaybe (Left SMPResponseTimeout) <$> timeout tcpTimeout a
|
||||
|
||||
send :: CorrId -> SignedRawTransmission -> STM (TMVar Response)
|
||||
send corrId t = do
|
||||
r <- newEmptyTMVar
|
||||
modifyTVar sentCommands . M.insert corrId $ Request qId r
|
||||
writeTBQueue sndQ t
|
||||
return r
|
||||
@@ -0,0 +1,384 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Crypto
|
||||
( PrivateKey (rsaPrivateKey),
|
||||
SafePrivateKey, -- constructor is not exported
|
||||
FullPrivateKey (..),
|
||||
PublicKey (..),
|
||||
Signature (..),
|
||||
CryptoError (..),
|
||||
SafeKeyPair,
|
||||
FullKeyPair,
|
||||
Key (..),
|
||||
IV (..),
|
||||
KeyHash (..),
|
||||
generateKeyPair,
|
||||
publicKey,
|
||||
publicKeySize,
|
||||
validKeySize,
|
||||
safePrivateKey,
|
||||
sign,
|
||||
verify,
|
||||
encrypt,
|
||||
decrypt,
|
||||
encryptOAEP,
|
||||
decryptOAEP,
|
||||
encryptAES,
|
||||
decryptAES,
|
||||
serializePrivKey,
|
||||
serializePubKey,
|
||||
encodePubKey,
|
||||
publicKeyHash,
|
||||
sha256Hash,
|
||||
privKeyP,
|
||||
pubKeyP,
|
||||
binaryPubKeyP,
|
||||
authTagSize,
|
||||
authTagToBS,
|
||||
bsToAuthTag,
|
||||
randomAesKey,
|
||||
randomIV,
|
||||
aesKeyP,
|
||||
ivP,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Cipher.AES (AES256)
|
||||
import qualified Crypto.Cipher.Types as AES
|
||||
import qualified Crypto.Error as CE
|
||||
import Crypto.Hash (Digest, SHA256 (..), hash)
|
||||
import Crypto.Number.Generate (generateMax)
|
||||
import Crypto.Number.Prime (findPrimeFrom)
|
||||
import qualified Crypto.PubKey.RSA as R
|
||||
import qualified Crypto.PubKey.RSA.OAEP as OAEP
|
||||
import qualified Crypto.PubKey.RSA.PSS as PSS
|
||||
import Crypto.Random (getRandomBytes)
|
||||
import Data.ASN1.BinaryEncoding
|
||||
import Data.ASN1.Encoding
|
||||
import Data.ASN1.Types
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Base64 (decode, encode)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.ByteString.Internal (c2w, w2c)
|
||||
import Data.ByteString.Lazy (fromStrict, toStrict)
|
||||
import Data.String
|
||||
import Data.X509
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Network.Transport.Internal (decodeWord32, encodeWord32)
|
||||
import Simplex.Messaging.Parsers (base64P, blobFieldParser, parseAll)
|
||||
import Simplex.Messaging.Util (liftEitherError, (<$?>))
|
||||
|
||||
newtype PublicKey = PublicKey {rsaPublicKey :: R.PublicKey} deriving (Eq, Show)
|
||||
|
||||
newtype SafePrivateKey = SafePrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show)
|
||||
|
||||
newtype FullPrivateKey = FullPrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show)
|
||||
|
||||
class PrivateKey k where
|
||||
rsaPrivateKey :: k -> R.PrivateKey
|
||||
_privateKey :: R.PrivateKey -> k
|
||||
mkPrivateKey :: R.PrivateKey -> k
|
||||
|
||||
instance PrivateKey SafePrivateKey where
|
||||
rsaPrivateKey = unPrivateKey
|
||||
_privateKey = SafePrivateKey
|
||||
mkPrivateKey R.PrivateKey {private_pub = k, private_d} =
|
||||
safePrivateKey (R.public_size k, R.public_n k, private_d)
|
||||
|
||||
instance PrivateKey FullPrivateKey where
|
||||
rsaPrivateKey = unPrivateKey
|
||||
_privateKey = FullPrivateKey
|
||||
mkPrivateKey = FullPrivateKey
|
||||
|
||||
instance IsString FullPrivateKey where
|
||||
fromString = parseString (decode >=> decodePrivKey)
|
||||
|
||||
instance IsString PublicKey where
|
||||
fromString = parseString (decode >=> decodePubKey)
|
||||
|
||||
parseString :: (ByteString -> Either String a) -> (String -> a)
|
||||
parseString parse = either error id . parse . B.pack
|
||||
|
||||
instance ToField SafePrivateKey where toField = toField . encodePrivKey
|
||||
|
||||
instance ToField PublicKey where toField = toField . encodePubKey
|
||||
|
||||
instance FromField SafePrivateKey where fromField = blobFieldParser binaryPrivKeyP
|
||||
|
||||
instance FromField PublicKey where fromField = blobFieldParser binaryPubKeyP
|
||||
|
||||
type KeyPair k = (PublicKey, k)
|
||||
|
||||
type SafeKeyPair = (PublicKey, SafePrivateKey)
|
||||
|
||||
type FullKeyPair = (PublicKey, FullPrivateKey)
|
||||
|
||||
newtype Signature = Signature {unSignature :: ByteString} deriving (Eq, Show)
|
||||
|
||||
instance IsString Signature where
|
||||
fromString = Signature . fromString
|
||||
|
||||
newtype Verified = Verified ByteString deriving (Show)
|
||||
|
||||
data CryptoError
|
||||
= RSAEncryptError R.Error
|
||||
| RSADecryptError R.Error
|
||||
| RSASignError R.Error
|
||||
| AESCipherError CE.CryptoError
|
||||
| CryptoIVError
|
||||
| AESDecryptError
|
||||
| CryptoLargeMsgError
|
||||
| CryptoHeaderError String
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
pubExpRange :: Integer
|
||||
pubExpRange = 2 ^ (1024 :: Int)
|
||||
|
||||
aesKeySize :: Int
|
||||
aesKeySize = 256 `div` 8
|
||||
|
||||
authTagSize :: Int
|
||||
authTagSize = 128 `div` 8
|
||||
|
||||
generateKeyPair :: PrivateKey k => Int -> IO (KeyPair k)
|
||||
generateKeyPair size = loop
|
||||
where
|
||||
publicExponent = findPrimeFrom . (+ 3) <$> generateMax pubExpRange
|
||||
loop = do
|
||||
(k, pk) <- R.generate size =<< publicExponent
|
||||
let n = R.public_n k
|
||||
d = R.private_d pk
|
||||
if d * d < n
|
||||
then loop
|
||||
else pure (PublicKey k, mkPrivateKey pk)
|
||||
|
||||
privateKeySize :: PrivateKey k => k -> Int
|
||||
privateKeySize = R.public_size . R.private_pub . rsaPrivateKey
|
||||
|
||||
publicKey :: FullPrivateKey -> PublicKey
|
||||
publicKey = PublicKey . R.private_pub . rsaPrivateKey
|
||||
|
||||
publicKeySize :: PublicKey -> Int
|
||||
publicKeySize = R.public_size . rsaPublicKey
|
||||
|
||||
validKeySize :: Int -> Bool
|
||||
validKeySize = \case
|
||||
128 -> True
|
||||
256 -> True
|
||||
512 -> True
|
||||
_ -> False
|
||||
|
||||
data Header = Header
|
||||
{ aesKey :: Key,
|
||||
ivBytes :: IV,
|
||||
authTag :: AES.AuthTag,
|
||||
msgSize :: Int
|
||||
}
|
||||
|
||||
newtype Key = Key {unKey :: ByteString}
|
||||
|
||||
newtype IV = IV {unIV :: ByteString}
|
||||
|
||||
newtype KeyHash = KeyHash {unKeyHash :: ByteString} deriving (Eq, Ord, Show)
|
||||
|
||||
instance IsString KeyHash where
|
||||
fromString = parseString . parseAll $ KeyHash <$> base64P
|
||||
|
||||
instance ToField KeyHash where toField = toField . encode . unKeyHash
|
||||
|
||||
instance FromField KeyHash where fromField = blobFieldParser $ KeyHash <$> base64P
|
||||
|
||||
publicKeyHash :: PublicKey -> KeyHash
|
||||
publicKeyHash = KeyHash . sha256Hash . encodePubKey
|
||||
|
||||
sha256Hash :: ByteString -> ByteString
|
||||
sha256Hash = BA.convert . (hash :: ByteString -> Digest SHA256)
|
||||
|
||||
serializeHeader :: Header -> ByteString
|
||||
serializeHeader Header {aesKey, ivBytes, authTag, msgSize} =
|
||||
unKey aesKey <> unIV ivBytes <> authTagToBS authTag <> (encodeWord32 . fromIntegral) msgSize
|
||||
|
||||
headerP :: Parser Header
|
||||
headerP = do
|
||||
aesKey <- aesKeyP
|
||||
ivBytes <- ivP
|
||||
authTag <- bsToAuthTag <$> A.take authTagSize
|
||||
msgSize <- fromIntegral . decodeWord32 <$> A.take 4
|
||||
return Header {aesKey, ivBytes, authTag, msgSize}
|
||||
|
||||
aesKeyP :: Parser Key
|
||||
aesKeyP = Key <$> A.take aesKeySize
|
||||
|
||||
ivP :: Parser IV
|
||||
ivP = IV <$> A.take (ivSize @AES256)
|
||||
|
||||
parseHeader :: ByteString -> Either CryptoError Header
|
||||
parseHeader = first CryptoHeaderError . parseAll headerP
|
||||
|
||||
encrypt :: PublicKey -> Int -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
encrypt k paddedSize msg = do
|
||||
aesKey <- liftIO randomAesKey
|
||||
ivBytes <- liftIO randomIV
|
||||
(authTag, msg') <- encryptAES aesKey ivBytes paddedSize msg
|
||||
let header = Header {aesKey, ivBytes, authTag, msgSize = B.length msg}
|
||||
encHeader <- encryptOAEP k $ serializeHeader header
|
||||
return $ encHeader <> msg'
|
||||
|
||||
decrypt :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
decrypt pk msg'' = do
|
||||
let (encHeader, msg') = B.splitAt (privateKeySize pk) msg''
|
||||
header <- decryptOAEP pk encHeader
|
||||
Header {aesKey, ivBytes, authTag, msgSize} <- except $ parseHeader header
|
||||
msg <- decryptAES aesKey ivBytes msg' authTag
|
||||
return $ B.take msgSize msg
|
||||
|
||||
encryptAES :: Key -> IV -> Int -> ByteString -> ExceptT CryptoError IO (AES.AuthTag, ByteString)
|
||||
encryptAES aesKey ivBytes paddedSize msg = do
|
||||
aead <- initAEAD @AES256 aesKey ivBytes
|
||||
msg' <- paddedMsg
|
||||
return $ AES.aeadSimpleEncrypt aead B.empty msg' authTagSize
|
||||
where
|
||||
len = B.length msg
|
||||
paddedMsg
|
||||
| len >= paddedSize = throwE CryptoLargeMsgError
|
||||
| otherwise = return (msg <> B.replicate (paddedSize - len) '#')
|
||||
|
||||
decryptAES :: Key -> IV -> ByteString -> AES.AuthTag -> ExceptT CryptoError IO ByteString
|
||||
decryptAES aesKey ivBytes msg authTag = do
|
||||
aead <- initAEAD @AES256 aesKey ivBytes
|
||||
maybeError AESDecryptError $ AES.aeadSimpleDecrypt aead B.empty msg authTag
|
||||
|
||||
initAEAD :: forall c. AES.BlockCipher c => Key -> IV -> ExceptT CryptoError IO (AES.AEAD c)
|
||||
initAEAD (Key aesKey) (IV ivBytes) = do
|
||||
iv <- makeIV @c ivBytes
|
||||
cryptoFailable $ do
|
||||
cipher <- AES.cipherInit aesKey
|
||||
AES.aeadInit AES.AEAD_GCM cipher iv
|
||||
|
||||
randomAesKey :: IO Key
|
||||
randomAesKey = Key <$> getRandomBytes aesKeySize
|
||||
|
||||
randomIV :: IO IV
|
||||
randomIV = IV <$> getRandomBytes (ivSize @AES256)
|
||||
|
||||
ivSize :: forall c. AES.BlockCipher c => Int
|
||||
ivSize = AES.blockSize (undefined :: c)
|
||||
|
||||
makeIV :: AES.BlockCipher c => ByteString -> ExceptT CryptoError IO (AES.IV c)
|
||||
makeIV bs = maybeError CryptoIVError $ AES.makeIV bs
|
||||
|
||||
maybeError :: CryptoError -> Maybe a -> ExceptT CryptoError IO a
|
||||
maybeError e = maybe (throwE e) return
|
||||
|
||||
authTagToBS :: AES.AuthTag -> ByteString
|
||||
authTagToBS = B.pack . map w2c . BA.unpack . AES.unAuthTag
|
||||
|
||||
bsToAuthTag :: ByteString -> AES.AuthTag
|
||||
bsToAuthTag = AES.AuthTag . BA.pack . map c2w . B.unpack
|
||||
|
||||
cryptoFailable :: CE.CryptoFailable a -> ExceptT CryptoError IO a
|
||||
cryptoFailable = liftEither . first AESCipherError . CE.eitherCryptoError
|
||||
|
||||
oaepParams :: OAEP.OAEPParams SHA256 ByteString ByteString
|
||||
oaepParams = OAEP.defaultOAEPParams SHA256
|
||||
|
||||
encryptOAEP :: PublicKey -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
encryptOAEP (PublicKey k) aesKey =
|
||||
liftEitherError RSAEncryptError $
|
||||
OAEP.encrypt oaepParams k aesKey
|
||||
|
||||
decryptOAEP :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
decryptOAEP pk encKey =
|
||||
liftEitherError RSADecryptError $
|
||||
OAEP.decryptSafer oaepParams (rsaPrivateKey pk) encKey
|
||||
|
||||
pssParams :: PSS.PSSParams SHA256 ByteString ByteString
|
||||
pssParams = PSS.defaultPSSParams SHA256
|
||||
|
||||
sign :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO Signature
|
||||
sign pk msg = ExceptT $ bimap RSASignError Signature <$> PSS.signSafer pssParams (rsaPrivateKey pk) msg
|
||||
|
||||
verify :: PublicKey -> Signature -> ByteString -> Bool
|
||||
verify (PublicKey k) (Signature sig) msg = PSS.verify pssParams k msg sig
|
||||
|
||||
serializePubKey :: PublicKey -> ByteString
|
||||
serializePubKey = ("rsa:" <>) . encode . encodePubKey
|
||||
|
||||
serializePrivKey :: PrivateKey k => k -> ByteString
|
||||
serializePrivKey = ("rsa:" <>) . encode . encodePrivKey
|
||||
|
||||
pubKeyP :: Parser PublicKey
|
||||
pubKeyP = decodePubKey <$?> ("rsa:" *> base64P)
|
||||
|
||||
binaryPubKeyP :: Parser PublicKey
|
||||
binaryPubKeyP = decodePubKey <$?> A.takeByteString
|
||||
|
||||
privKeyP :: PrivateKey k => Parser k
|
||||
privKeyP = decodePrivKey <$?> ("rsa:" *> base64P)
|
||||
|
||||
binaryPrivKeyP :: PrivateKey k => Parser k
|
||||
binaryPrivKeyP = decodePrivKey <$?> A.takeByteString
|
||||
|
||||
safePrivateKey :: (Int, Integer, Integer) -> SafePrivateKey
|
||||
safePrivateKey = SafePrivateKey . safeRsaPrivateKey
|
||||
|
||||
safeRsaPrivateKey :: (Int, Integer, Integer) -> R.PrivateKey
|
||||
safeRsaPrivateKey (size, n, d) =
|
||||
R.PrivateKey
|
||||
{ private_pub =
|
||||
R.PublicKey
|
||||
{ public_size = size,
|
||||
public_n = n,
|
||||
public_e = 0
|
||||
},
|
||||
private_d = d,
|
||||
private_p = 0,
|
||||
private_q = 0,
|
||||
private_dP = 0,
|
||||
private_dQ = 0,
|
||||
private_qinv = 0
|
||||
}
|
||||
|
||||
encodePubKey :: PublicKey -> ByteString
|
||||
encodePubKey = encodeKey . PubKeyRSA . rsaPublicKey
|
||||
|
||||
encodePrivKey :: PrivateKey k => k -> ByteString
|
||||
encodePrivKey = encodeKey . PrivKeyRSA . rsaPrivateKey
|
||||
|
||||
encodeKey :: ASN1Object a => a -> ByteString
|
||||
encodeKey k = toStrict . encodeASN1 DER $ toASN1 k []
|
||||
|
||||
decodePubKey :: ByteString -> Either String PublicKey
|
||||
decodePubKey =
|
||||
decodeKey >=> \case
|
||||
(PubKeyRSA k, []) -> Right $ PublicKey k
|
||||
r -> keyError r
|
||||
|
||||
decodePrivKey :: PrivateKey k => ByteString -> Either String k
|
||||
decodePrivKey =
|
||||
decodeKey >=> \case
|
||||
(PrivKeyRSA pk, []) -> Right $ mkPrivateKey pk
|
||||
r -> keyError r
|
||||
|
||||
decodeKey :: ASN1Object a => ByteString -> Either String (a, [ASN1])
|
||||
decodeKey = fromASN1 <=< first show . decodeASN1 DER . fromStrict
|
||||
|
||||
keyError :: (a, [ASN1]) -> Either String b
|
||||
keyError = \case
|
||||
(_, []) -> Left "not RSA key"
|
||||
_ -> Left "more than one key"
|
||||
@@ -0,0 +1,59 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Parsers where
|
||||
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isAlphaNum)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.ISO8601 (parseISO8601)
|
||||
import Data.Typeable (Typeable)
|
||||
import Database.SQLite.Simple (ResultError (..), SQLData (..))
|
||||
import Database.SQLite.Simple.FromField (FieldParser, returnError)
|
||||
import Database.SQLite.Simple.Internal (Field (..))
|
||||
import Database.SQLite.Simple.Ok (Ok (Ok))
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
base64P :: Parser ByteString
|
||||
base64P = decode <$?> base64StringP
|
||||
|
||||
base64StringP :: Parser ByteString
|
||||
base64StringP = do
|
||||
str <- A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
|
||||
pad <- A.takeWhile (== '=')
|
||||
pure $ str <> pad
|
||||
|
||||
tsISO8601P :: Parser UTCTime
|
||||
tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill (== ' ')
|
||||
|
||||
parse :: Parser a -> e -> (ByteString -> Either e a)
|
||||
parse parser err = first (const err) . parseAll parser
|
||||
|
||||
parseAll :: Parser a -> (ByteString -> Either String a)
|
||||
parseAll parser = A.parseOnly (parser <* A.endOfInput)
|
||||
|
||||
parseRead :: Read a => Parser ByteString -> Parser a
|
||||
parseRead = (>>= maybe (fail "cannot read") pure . readMaybe . B.unpack)
|
||||
|
||||
parseRead1 :: Read a => Parser a
|
||||
parseRead1 = parseRead $ A.takeTill (== ' ')
|
||||
|
||||
parseRead2 :: Read a => Parser a
|
||||
parseRead2 = parseRead $ do
|
||||
w1 <- A.takeTill (== ' ') <* A.char ' '
|
||||
w2 <- A.takeTill (== ' ')
|
||||
pure $ w1 <> " " <> w2
|
||||
|
||||
blobFieldParser :: Typeable k => Parser k -> FieldParser k
|
||||
blobFieldParser p = \case
|
||||
f@(Field (SQLBlob b) _) ->
|
||||
case parseAll p b of
|
||||
Right k -> Ok k
|
||||
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
|
||||
f -> returnError ConversionFailed f "expecting SQLBlob column type"
|
||||
@@ -0,0 +1,276 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Protocol where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Kind
|
||||
import Data.String
|
||||
import Data.Time.Clock
|
||||
import Data.Time.ISO8601
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
|
||||
data Party = Broker | Recipient | Sender
|
||||
deriving (Show)
|
||||
|
||||
data SParty :: Party -> Type where
|
||||
SBroker :: SParty Broker
|
||||
SRecipient :: SParty Recipient
|
||||
SSender :: SParty Sender
|
||||
|
||||
deriving instance Show (SParty a)
|
||||
|
||||
data Cmd = forall a. Cmd (SParty a) (Command a)
|
||||
|
||||
deriving instance Show Cmd
|
||||
|
||||
type Transmission = (CorrId, QueueId, Cmd)
|
||||
|
||||
type SignedTransmission = (C.Signature, Transmission)
|
||||
|
||||
type TransmissionOrError = (CorrId, QueueId, Either ErrorType Cmd)
|
||||
|
||||
type SignedTransmissionOrError = (C.Signature, TransmissionOrError)
|
||||
|
||||
type RawTransmission = (ByteString, ByteString, ByteString, ByteString)
|
||||
|
||||
type SignedRawTransmission = (C.Signature, ByteString)
|
||||
|
||||
type RecipientId = QueueId
|
||||
|
||||
type SenderId = QueueId
|
||||
|
||||
type QueueId = Encoded
|
||||
|
||||
data Command (a :: Party) where
|
||||
-- SMP recipient commands
|
||||
NEW :: RecipientPublicKey -> Command Recipient
|
||||
SUB :: Command Recipient
|
||||
KEY :: SenderPublicKey -> Command Recipient
|
||||
ACK :: Command Recipient
|
||||
OFF :: Command Recipient
|
||||
DEL :: Command Recipient
|
||||
-- SMP sender commands
|
||||
SEND :: MsgBody -> Command Sender
|
||||
PING :: Command Sender
|
||||
-- SMP broker commands (responses, messages, notifications)
|
||||
IDS :: RecipientId -> SenderId -> Command Broker
|
||||
MSG :: MsgId -> UTCTime -> MsgBody -> Command Broker
|
||||
END :: Command Broker
|
||||
OK :: Command Broker
|
||||
ERR :: ErrorType -> Command Broker
|
||||
PONG :: Command Broker
|
||||
|
||||
deriving instance Show (Command a)
|
||||
|
||||
deriving instance Eq (Command a)
|
||||
|
||||
type Encoded = ByteString
|
||||
|
||||
-- newtype to avoid accidentally changing order of transmission parts
|
||||
newtype CorrId = CorrId {bs :: ByteString} deriving (Eq, Ord, Show)
|
||||
|
||||
instance IsString CorrId where
|
||||
fromString = CorrId . fromString
|
||||
|
||||
-- only used by Agent, kept here so its definition is close to respective public key
|
||||
type RecipientPrivateKey = C.SafePrivateKey
|
||||
|
||||
type RecipientPublicKey = C.PublicKey
|
||||
|
||||
-- only used by Agent, kept here so its definition is close to respective public key
|
||||
type SenderPrivateKey = C.SafePrivateKey
|
||||
|
||||
type SenderPublicKey = C.PublicKey
|
||||
|
||||
type MsgId = Encoded
|
||||
|
||||
type MsgBody = ByteString
|
||||
|
||||
data ErrorType
|
||||
= BLOCK
|
||||
| CMD CommandError
|
||||
| AUTH
|
||||
| NO_MSG
|
||||
| INTERNAL
|
||||
| DUPLICATE_ -- TODO remove, not part of SMP protocol
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
data CommandError
|
||||
= PROHIBITED
|
||||
| KEY_SIZE
|
||||
| SYNTAX
|
||||
| NO_AUTH
|
||||
| HAS_AUTH
|
||||
| NO_QUEUE
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
instance Arbitrary ErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary CommandError where arbitrary = genericArbitraryU
|
||||
|
||||
transmissionP :: Parser RawTransmission
|
||||
transmissionP = do
|
||||
signature <- segment
|
||||
corrId <- segment
|
||||
queueId <- segment
|
||||
command <- A.takeByteString
|
||||
return (signature, corrId, queueId, command)
|
||||
where
|
||||
segment = A.takeTill (== ' ') <* " "
|
||||
|
||||
commandP :: Parser Cmd
|
||||
commandP =
|
||||
"NEW " *> newCmd
|
||||
<|> "IDS " *> idsResp
|
||||
<|> "SUB" $> Cmd SRecipient SUB
|
||||
<|> "KEY " *> keyCmd
|
||||
<|> "ACK" $> Cmd SRecipient ACK
|
||||
<|> "OFF" $> Cmd SRecipient OFF
|
||||
<|> "DEL" $> Cmd SRecipient DEL
|
||||
<|> "SEND " *> sendCmd
|
||||
<|> "PING" $> Cmd SSender PING
|
||||
<|> "MSG " *> message
|
||||
<|> "END" $> Cmd SBroker END
|
||||
<|> "OK" $> Cmd SBroker OK
|
||||
<|> "ERR " *> serverError
|
||||
<|> "PONG" $> Cmd SBroker PONG
|
||||
where
|
||||
newCmd = Cmd SRecipient . NEW <$> C.pubKeyP
|
||||
idsResp = Cmd SBroker <$> (IDS <$> (base64P <* A.space) <*> base64P)
|
||||
keyCmd = Cmd SRecipient . KEY <$> C.pubKeyP
|
||||
sendCmd = do
|
||||
size <- A.decimal <* A.space
|
||||
Cmd SSender . SEND <$> A.take size <* A.space
|
||||
message = do
|
||||
msgId <- base64P <* A.space
|
||||
ts <- tsISO8601P <* A.space
|
||||
size <- A.decimal <* A.space
|
||||
Cmd SBroker . MSG msgId ts <$> A.take size <* A.space
|
||||
serverError = Cmd SBroker . ERR <$> errorTypeP
|
||||
|
||||
-- TODO ignore the end of block, no need to parse it
|
||||
parseCommand :: ByteString -> Either ErrorType Cmd
|
||||
parseCommand = parse (commandP <* " " <* A.takeByteString) $ CMD SYNTAX
|
||||
|
||||
serializeCommand :: Cmd -> ByteString
|
||||
serializeCommand = \case
|
||||
Cmd SRecipient (NEW rKey) -> "NEW " <> C.serializePubKey rKey
|
||||
Cmd SRecipient (KEY sKey) -> "KEY " <> C.serializePubKey sKey
|
||||
Cmd SRecipient cmd -> bshow cmd
|
||||
Cmd SSender (SEND msgBody) -> "SEND " <> serializeMsg msgBody
|
||||
Cmd SSender PING -> "PING"
|
||||
Cmd SBroker (MSG msgId ts msgBody) ->
|
||||
B.unwords ["MSG", encode msgId, B.pack $ formatISO8601Millis ts, serializeMsg msgBody]
|
||||
Cmd SBroker (IDS rId sId) -> B.unwords ["IDS", encode rId, encode sId]
|
||||
Cmd SBroker (ERR err) -> "ERR " <> serializeErrorType err
|
||||
Cmd SBroker resp -> bshow resp
|
||||
where
|
||||
serializeMsg msgBody = bshow (B.length msgBody) <> " " <> msgBody <> " "
|
||||
|
||||
errorTypeP :: Parser ErrorType
|
||||
errorTypeP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
|
||||
|
||||
serializeErrorType :: ErrorType -> ByteString
|
||||
serializeErrorType = bshow
|
||||
|
||||
tPut :: THandle -> SignedRawTransmission -> IO (Either TransportError ())
|
||||
tPut th (C.Signature sig, t) =
|
||||
tPutEncrypted th $ encode sig <> " " <> t <> " "
|
||||
|
||||
serializeTransmission :: Transmission -> ByteString
|
||||
serializeTransmission (CorrId corrId, queueId, command) =
|
||||
B.intercalate " " [corrId, encode queueId, serializeCommand command]
|
||||
|
||||
fromClient :: Cmd -> Either ErrorType Cmd
|
||||
fromClient = \case
|
||||
Cmd SBroker _ -> Left $ CMD PROHIBITED
|
||||
cmd -> Right cmd
|
||||
|
||||
fromServer :: Cmd -> Either ErrorType Cmd
|
||||
fromServer = \case
|
||||
cmd@(Cmd SBroker _) -> Right cmd
|
||||
_ -> Left $ CMD PROHIBITED
|
||||
|
||||
tGetParse :: THandle -> IO (Either TransportError RawTransmission)
|
||||
tGetParse th = (>>= parse transmissionP TEBadBlock) <$> tGetEncrypted th
|
||||
|
||||
-- | get client and server transmissions
|
||||
-- `fromParty` is used to limit allowed senders - `fromClient` or `fromServer` should be used
|
||||
tGet :: forall m. MonadIO m => (Cmd -> Either ErrorType Cmd) -> THandle -> m SignedTransmissionOrError
|
||||
tGet fromParty th = liftIO (tGetParse th) >>= decodeParseValidate
|
||||
where
|
||||
decodeParseValidate :: Either TransportError RawTransmission -> m SignedTransmissionOrError
|
||||
decodeParseValidate = \case
|
||||
Right (signature, corrId, queueId, command) ->
|
||||
let decodedTransmission = liftM2 (,corrId,,command) (validSig =<< decode signature) (decode queueId)
|
||||
in either (const $ tError corrId) tParseValidate decodedTransmission
|
||||
Left _ -> tError ""
|
||||
|
||||
validSig :: ByteString -> Either String ByteString
|
||||
validSig sig
|
||||
| B.null sig || C.validKeySize (B.length sig) = Right sig
|
||||
| otherwise = Left "invalid signature size"
|
||||
|
||||
tError :: ByteString -> m SignedTransmissionOrError
|
||||
tError corrId = return (C.Signature "", (CorrId corrId, "", Left BLOCK))
|
||||
|
||||
tParseValidate :: RawTransmission -> m SignedTransmissionOrError
|
||||
tParseValidate t@(sig, corrId, queueId, command) = do
|
||||
let cmd = parseCommand command >>= fromParty >>= tCredentials t
|
||||
return (C.Signature sig, (CorrId corrId, queueId, cmd))
|
||||
|
||||
tCredentials :: RawTransmission -> Cmd -> Either ErrorType Cmd
|
||||
tCredentials (signature, _, queueId, _) cmd = case cmd of
|
||||
-- IDS response must not have queue ID
|
||||
Cmd SBroker (IDS _ _) -> Right cmd
|
||||
-- ERR response does not always have queue ID
|
||||
Cmd SBroker (ERR _) -> Right cmd
|
||||
-- PONG response must not have queue ID
|
||||
Cmd SBroker PONG
|
||||
| B.null queueId -> Right cmd
|
||||
| otherwise -> Left $ CMD HAS_AUTH
|
||||
-- other responses must have queue ID
|
||||
Cmd SBroker _
|
||||
| B.null queueId -> Left $ CMD NO_QUEUE
|
||||
| otherwise -> Right cmd
|
||||
-- NEW must have signature but NOT queue ID
|
||||
Cmd SRecipient (NEW _)
|
||||
| B.null signature -> Left $ CMD NO_AUTH
|
||||
| not (B.null queueId) -> Left $ CMD HAS_AUTH
|
||||
| otherwise -> Right cmd
|
||||
-- SEND must have queue ID, signature is not always required
|
||||
Cmd SSender (SEND _)
|
||||
| B.null queueId -> Left $ CMD NO_QUEUE
|
||||
| otherwise -> Right cmd
|
||||
-- PING must not have queue ID or signature
|
||||
Cmd SSender PING
|
||||
| B.null queueId && B.null signature -> Right cmd
|
||||
| otherwise -> Left $ CMD HAS_AUTH
|
||||
-- other client commands must have both signature and queue ID
|
||||
Cmd SRecipient _
|
||||
| B.null signature || B.null queueId -> Left $ CMD NO_AUTH
|
||||
| otherwise -> Right cmd
|
||||
@@ -0,0 +1,343 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
-- TODO move randomBytes to another module
|
||||
module Simplex.Messaging.Server (runSMPServer, runSMPServerBlocking, randomBytes) where
|
||||
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Time.Clock
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Simplex.Messaging.Server.MsgStore
|
||||
import Simplex.Messaging.Server.MsgStore.STM (MsgQueue)
|
||||
import Simplex.Messaging.Server.QueueStore
|
||||
import Simplex.Messaging.Server.QueueStore.STM (QueueStore)
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util
|
||||
import UnliftIO.Async
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Exception
|
||||
import UnliftIO.IO
|
||||
import UnliftIO.STM
|
||||
|
||||
runSMPServer :: (MonadRandom m, MonadUnliftIO m) => ServerConfig -> m ()
|
||||
runSMPServer cfg = newEmptyTMVarIO >>= (`runSMPServerBlocking` cfg)
|
||||
|
||||
runSMPServerBlocking :: (MonadRandom m, MonadUnliftIO m) => TMVar Bool -> ServerConfig -> m ()
|
||||
runSMPServerBlocking started cfg@ServerConfig {tcpPort} = do
|
||||
env <- newEnv cfg
|
||||
runReaderT smpServer env
|
||||
where
|
||||
smpServer :: (MonadUnliftIO m, MonadReader Env m) => m ()
|
||||
smpServer = do
|
||||
s <- asks server
|
||||
race_ (runTCPServer started tcpPort runClient) (serverThread s)
|
||||
`finally` withLog closeStoreLog
|
||||
|
||||
serverThread :: MonadUnliftIO m => Server -> m ()
|
||||
serverThread Server {subscribedQ, subscribers} = forever . atomically $ do
|
||||
(rId, clnt) <- readTBQueue subscribedQ
|
||||
cs <- readTVar subscribers
|
||||
case M.lookup rId cs of
|
||||
Just Client {rcvQ} -> writeTBQueue rcvQ (CorrId B.empty, rId, Cmd SBroker END)
|
||||
Nothing -> return ()
|
||||
writeTVar subscribers $ M.insert rId clnt cs
|
||||
|
||||
runClient :: (MonadUnliftIO m, MonadReader Env m) => Handle -> m ()
|
||||
runClient h = do
|
||||
keyPair <- asks serverKeyPair
|
||||
liftIO (runExceptT $ serverHandshake h keyPair) >>= \case
|
||||
Right th -> runClientTransport th
|
||||
Left _ -> pure ()
|
||||
|
||||
runClientTransport :: (MonadUnliftIO m, MonadReader Env m) => THandle -> m ()
|
||||
runClientTransport th = do
|
||||
q <- asks $ tbqSize . config
|
||||
c <- atomically $ newClient q
|
||||
s <- asks server
|
||||
raceAny_ [send th c, client c s, receive th c]
|
||||
`finally` cancelSubscribers c
|
||||
|
||||
cancelSubscribers :: MonadUnliftIO m => Client -> m ()
|
||||
cancelSubscribers Client {subscriptions} =
|
||||
readTVarIO subscriptions >>= mapM_ cancelSub
|
||||
|
||||
cancelSub :: MonadUnliftIO m => Sub -> m ()
|
||||
cancelSub = \case
|
||||
Sub {subThread = SubThread t} -> killThread t
|
||||
_ -> return ()
|
||||
|
||||
receive :: (MonadUnliftIO m, MonadReader Env m) => THandle -> Client -> m ()
|
||||
receive h Client {rcvQ} = forever $ do
|
||||
(signature, (corrId, queueId, cmdOrError)) <- tGet fromClient h
|
||||
t <- case cmdOrError of
|
||||
Left e -> return . mkResp corrId queueId $ ERR e
|
||||
Right cmd -> verifyTransmission (signature, (corrId, queueId, cmd))
|
||||
atomically $ writeTBQueue rcvQ t
|
||||
|
||||
send :: MonadUnliftIO m => THandle -> Client -> m ()
|
||||
send h Client {sndQ} = forever $ do
|
||||
t <- atomically $ readTBQueue sndQ
|
||||
liftIO $ tPut h ("", serializeTransmission t)
|
||||
|
||||
mkResp :: CorrId -> QueueId -> Command 'Broker -> Transmission
|
||||
mkResp corrId queueId command = (corrId, queueId, Cmd SBroker command)
|
||||
|
||||
verifyTransmission :: forall m. (MonadUnliftIO m, MonadReader Env m) => SignedTransmission -> m Transmission
|
||||
verifyTransmission (sig, t@(corrId, queueId, cmd)) = do
|
||||
(corrId,queueId,) <$> case cmd of
|
||||
Cmd SBroker _ -> return $ smpErr INTERNAL -- it can only be client command, because `fromClient` was used
|
||||
Cmd SRecipient (NEW k) -> pure $ verifySignature k
|
||||
Cmd SRecipient _ -> verifyCmd SRecipient $ verifySignature . recipientKey
|
||||
Cmd SSender (SEND _) -> verifyCmd SSender $ verifySend sig . senderKey
|
||||
Cmd SSender PING -> return cmd
|
||||
where
|
||||
verifyCmd :: SParty p -> (QueueRec -> Cmd) -> m Cmd
|
||||
verifyCmd party f = do
|
||||
st <- asks queueStore
|
||||
q <- atomically $ getQueue st party queueId
|
||||
pure $ either (const $ dummyVerify authErr) f q
|
||||
verifySend :: C.Signature -> Maybe SenderPublicKey -> Cmd
|
||||
verifySend "" = maybe cmd (const authErr)
|
||||
verifySend _ = maybe authErr verifySignature
|
||||
verifySignature :: C.PublicKey -> Cmd
|
||||
verifySignature key = if verify key then cmd else authErr
|
||||
verify key
|
||||
| C.publicKeySize key == sigLen = cryptoVerify key
|
||||
| otherwise = dummyVerify False
|
||||
cryptoVerify key = C.verify key sig (serializeTransmission t)
|
||||
smpErr = Cmd SBroker . ERR
|
||||
authErr = smpErr AUTH
|
||||
dummyVerify :: a -> a
|
||||
dummyVerify = seq $
|
||||
cryptoVerify $ case sigLen of
|
||||
128 -> dummyKey128
|
||||
256 -> dummyKey256
|
||||
512 -> dummyKey512
|
||||
_ -> dummyKey256
|
||||
sigLen = B.length $ C.unSignature sig
|
||||
|
||||
-- These dummy keys are used with `dummyVerify` function to mitigate timing attacks
|
||||
-- by having the same time of the response whether a queue exists or nor, for all valid key/signature sizes
|
||||
dummyKey128 :: C.PublicKey
|
||||
dummyKey128 = "MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKBgQC2oeA7s4roXN5K2N6022I1/2CTeMKjWH0m00bSZWa4N8LDKeFcShh8YUxZea5giAveViTRNOOVLgcuXbKvR3u24szN04xP0+KnYUuUUIIoT3YSjX0IlomhDhhSyup4BmA0gAZ+D1OaIKZFX6J8yQ1Lr/JGLEfSRsBjw8l+4hs9OwKBgQDKA+YlZvGb3BcpDwKmatiCXN7ZRDWkjXbj8VAW5zV95tSRCCVN48hrFM1H4Ju2QMMUc6kPUVX+eW4ZjdCl5blIqIHMcTmsdcmsDDCg3PjUNrwc6bv/1TcirbAKcmnKt9iurIt6eerxSO7TZUXXMUVsi7eRwb/RUNhpCrpJ/hpIOw=="
|
||||
|
||||
dummyKey256 :: C.PublicKey
|
||||
dummyKey256 = "MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAxwmTvaqmdTbkfUGNi8Yu0L/T4cxuOlQlx3zGZ9X9Qx0+oZjknWK+QHrdWTcpS+zH4Hi7fP6kanOQoQ90Hj6Ghl57VU1GEdUPywSw4i1/7t0Wv9uT9Q2ktHp2rqVo3xkC9IVIpL7EZAxdRviIN2OsOB3g4a/F1ZpjxcAaZeOMUugiAX1+GtkLuE0Xn4neYjCaOghLxQTdhybN70VtnkiQLx/X9NjkDIl/spYGm3tQFMyYKkP6IWoEpj0926hJ0fmlmhy8tAOhlZsb/baW5cgkEZ3E9jVVrySCgQzoLQgma610FIISRpRJbSyv26jU7MkMxiyuBiDaFOORkXFttoKbtQKBgEbDS9II2brsz+vfI7uP8atFcawkE52cx4M1UWQhqb1H3tBiRl+qO+dMq1pPQF2bW7dlZAWYzS4W/367bTAuALHBDGB8xi1P4Njhh9vaOgTvuqrHG9NJQ85BLy0qGw8rjIWSIXVmVpfrXFJ8po5l04UE258Ll2yocv3QRQmddQW9"
|
||||
|
||||
dummyKey512 :: C.PublicKey
|
||||
dummyKey512 = "MIICoDANBgkqhkiG9w0BAQEFAAOCAo0AMIICiAKCAgEArkCY9DuverJ4mmzDektv9aZMFyeRV46WZK9NsOBKEc+1ncqMs+LhLti9asKNgUBRbNzmbOe0NYYftrUpwnATaenggkTFxxbJ4JGJuGYbsEdFWkXSvrbWGtM8YUmn5RkAGme12xQ89bSM4VoJAGnrYPHwmcQd+KYCPZvTUsxaxgrJTX65ejHN9BsAn8XtGViOtHTDJO9yUMD2WrJvd7wnNa+0ugEteDLzMU++xS98VC+uA1vfauUqi3yXVchdfrLdVUuM+JE0gUEXCgzjuHkaoHiaGNiGhdPYoAJJdOKQOIHAKdk7Th6OPhirPhc9XYNB4O8JDthKhNtfokvFIFlC4QBRzJhpLIENaEBDt08WmgpOnecZB/CuxkqqOrNa8j5K5jNrtXAI67W46VEC2jeQy/gZwb64Zit2A4D00xXzGbQTPGj4ehcEMhLx5LSCygViEf0w0tN3c3TEyUcgPzvECd2ZVpQLr9Z4a07Ebr+YSuxcHhjg4Rg1VyJyOTTvaCBGm5X2B3+tI4NUttmikIHOYpBnsLmHY2BgfH2KcrIsDyAhInXmTFr/L2+erFarUnlfATd2L8Ti43TNHDedO6k6jI5Gyi62yPwjqPLEIIK8l+pIeNfHJ3pPmjhHBfzFcQLMMMXffHWNK8kWklrQXK+4j4HiPcTBvlO1FEtG9nEIZhUCgYA4a6WtI2k5YNli1C89GY5rGUY7RP71T6RWri/D3Lz9T7GvU+FemAyYmsvCQwqijUOur0uLvwSP8VdxpSUcrjJJSWur2hrPWzWlu0XbNaeizxpFeKbQP+zSrWJ1z8RwfAeUjShxt8q1TuqGqY10wQyp3nyiTGvS+KwZVj5h5qx8NQ=="
|
||||
|
||||
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => Client -> Server -> m ()
|
||||
client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= processCommand
|
||||
>>= atomically . writeTBQueue sndQ
|
||||
where
|
||||
processCommand :: Transmission -> m Transmission
|
||||
processCommand (corrId, queueId, cmd) = do
|
||||
st <- asks queueStore
|
||||
case cmd of
|
||||
Cmd SBroker END -> unsubscribeQueue $> (corrId, queueId, cmd)
|
||||
Cmd SBroker _ -> return (corrId, queueId, cmd)
|
||||
Cmd SSender command -> case command of
|
||||
SEND msgBody -> sendMessage st msgBody
|
||||
PING -> return (corrId, queueId, Cmd SBroker PONG)
|
||||
Cmd SRecipient command -> case command of
|
||||
NEW rKey -> createQueue st rKey
|
||||
SUB -> subscribeQueue queueId
|
||||
ACK -> acknowledgeMsg
|
||||
KEY sKey -> secureQueue_ st sKey
|
||||
OFF -> suspendQueue_ st
|
||||
DEL -> delQueueAndMsgs st
|
||||
where
|
||||
createQueue :: QueueStore -> RecipientPublicKey -> m Transmission
|
||||
createQueue st rKey =
|
||||
checkKeySize rKey addSubscribe
|
||||
where
|
||||
addSubscribe =
|
||||
addQueueRetry 3 >>= \case
|
||||
Left e -> return $ ERR e
|
||||
Right (rId, sId) -> do
|
||||
withLog (`logCreateById` rId)
|
||||
subscribeQueue rId $> IDS rId sId
|
||||
|
||||
addQueueRetry :: Int -> m (Either ErrorType (RecipientId, SenderId))
|
||||
addQueueRetry 0 = return $ Left INTERNAL
|
||||
addQueueRetry n = do
|
||||
ids <- getIds
|
||||
atomically (addQueue st rKey ids) >>= \case
|
||||
Left DUPLICATE_ -> addQueueRetry $ n - 1
|
||||
Left e -> return $ Left e
|
||||
Right _ -> return $ Right ids
|
||||
|
||||
logCreateById :: StoreLog 'WriteMode -> RecipientId -> IO ()
|
||||
logCreateById s rId =
|
||||
atomically (getQueue st SRecipient rId) >>= \case
|
||||
Right q -> logCreateQueue s q
|
||||
_ -> pure ()
|
||||
|
||||
getIds :: m (RecipientId, SenderId)
|
||||
getIds = do
|
||||
n <- asks $ queueIdBytes . config
|
||||
liftM2 (,) (randomId n) (randomId n)
|
||||
|
||||
secureQueue_ :: QueueStore -> SenderPublicKey -> m Transmission
|
||||
secureQueue_ st sKey = do
|
||||
withLog $ \s -> logSecureQueue s queueId sKey
|
||||
atomically . checkKeySize sKey $ either ERR (const OK) <$> secureQueue st queueId sKey
|
||||
|
||||
checkKeySize :: Monad m' => C.PublicKey -> m' (Command 'Broker) -> m' Transmission
|
||||
checkKeySize key action =
|
||||
mkResp corrId queueId
|
||||
<$> if C.validKeySize $ C.publicKeySize key
|
||||
then action
|
||||
else pure . ERR $ CMD KEY_SIZE
|
||||
|
||||
suspendQueue_ :: QueueStore -> m Transmission
|
||||
suspendQueue_ st = do
|
||||
withLog (`logDeleteQueue` queueId)
|
||||
okResp <$> atomically (suspendQueue st queueId)
|
||||
|
||||
subscribeQueue :: RecipientId -> m Transmission
|
||||
subscribeQueue rId =
|
||||
atomically (getSubscription rId) >>= deliverMessage tryPeekMsg rId
|
||||
|
||||
getSubscription :: RecipientId -> STM Sub
|
||||
getSubscription rId = do
|
||||
subs <- readTVar subscriptions
|
||||
case M.lookup rId subs of
|
||||
Just s -> tryTakeTMVar (delivered s) $> s
|
||||
Nothing -> do
|
||||
writeTBQueue subscribedQ (rId, clnt)
|
||||
s <- newSubscription
|
||||
writeTVar subscriptions $ M.insert rId s subs
|
||||
return s
|
||||
|
||||
unsubscribeQueue :: m ()
|
||||
unsubscribeQueue = do
|
||||
sub <- atomically . stateTVar subscriptions $
|
||||
\cs -> (M.lookup queueId cs, M.delete queueId cs)
|
||||
mapM_ cancelSub sub
|
||||
|
||||
acknowledgeMsg :: m Transmission
|
||||
acknowledgeMsg =
|
||||
atomically (withSub queueId $ \s -> const s <$$> tryTakeTMVar (delivered s))
|
||||
>>= \case
|
||||
Just (Just s) -> deliverMessage tryDelPeekMsg queueId s
|
||||
_ -> return $ err NO_MSG
|
||||
|
||||
withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a)
|
||||
withSub rId f = readTVar subscriptions >>= mapM f . M.lookup rId
|
||||
|
||||
sendMessage :: QueueStore -> MsgBody -> m Transmission
|
||||
sendMessage st msgBody = do
|
||||
qr <- atomically $ getQueue st SSender queueId
|
||||
either (return . err) storeMessage qr
|
||||
where
|
||||
mkMessage :: m Message
|
||||
mkMessage = do
|
||||
msgId <- asks (msgIdBytes . config) >>= randomId
|
||||
ts <- liftIO getCurrentTime
|
||||
return $ Message {msgId, ts, msgBody}
|
||||
|
||||
storeMessage :: QueueRec -> m Transmission
|
||||
storeMessage qr = case status qr of
|
||||
QueueOff -> return $ err AUTH
|
||||
QueueActive -> do
|
||||
ms <- asks msgStore
|
||||
msg <- mkMessage
|
||||
atomically $ do
|
||||
q <- getMsgQueue ms (recipientId qr)
|
||||
writeMsg q msg
|
||||
return ok
|
||||
|
||||
deliverMessage :: (MsgQueue -> STM (Maybe Message)) -> RecipientId -> Sub -> m Transmission
|
||||
deliverMessage tryPeek rId = \case
|
||||
Sub {subThread = NoSub} -> do
|
||||
ms <- asks msgStore
|
||||
q <- atomically $ getMsgQueue ms rId
|
||||
atomically (tryPeek q) >>= \case
|
||||
Nothing -> forkSub q $> ok
|
||||
Just msg -> atomically setDelivered $> mkResp corrId rId (msgCmd msg)
|
||||
_ -> return ok
|
||||
where
|
||||
forkSub :: MsgQueue -> m ()
|
||||
forkSub q = do
|
||||
atomically . setSub $ \s -> s {subThread = SubPending}
|
||||
t <- forkIO $ subscriber q
|
||||
atomically . setSub $ \case
|
||||
s@Sub {subThread = SubPending} -> s {subThread = SubThread t}
|
||||
s -> s
|
||||
|
||||
subscriber :: MsgQueue -> m ()
|
||||
subscriber q = atomically $ do
|
||||
msg <- peekMsg q
|
||||
writeTBQueue sndQ $ mkResp (CorrId B.empty) rId (msgCmd msg)
|
||||
setSub (\s -> s {subThread = NoSub})
|
||||
void setDelivered
|
||||
|
||||
setSub :: (Sub -> Sub) -> STM ()
|
||||
setSub f = modifyTVar subscriptions $ M.adjust f rId
|
||||
|
||||
setDelivered :: STM (Maybe Bool)
|
||||
setDelivered = withSub rId $ \s -> tryPutTMVar (delivered s) ()
|
||||
|
||||
delQueueAndMsgs :: QueueStore -> m Transmission
|
||||
delQueueAndMsgs st = do
|
||||
withLog (`logDeleteQueue` queueId)
|
||||
ms <- asks msgStore
|
||||
atomically $
|
||||
deleteQueue st queueId >>= \case
|
||||
Left e -> return $ err e
|
||||
Right _ -> delMsgQueue ms queueId $> ok
|
||||
|
||||
ok :: Transmission
|
||||
ok = mkResp corrId queueId OK
|
||||
|
||||
err :: ErrorType -> Transmission
|
||||
err = mkResp corrId queueId . ERR
|
||||
|
||||
okResp :: Either ErrorType () -> Transmission
|
||||
okResp = either err $ const ok
|
||||
|
||||
msgCmd :: Message -> Command 'Broker
|
||||
msgCmd Message {msgId, ts, msgBody} = MSG msgId ts msgBody
|
||||
|
||||
withLog :: (MonadUnliftIO m, MonadReader Env m) => (StoreLog 'WriteMode -> IO a) -> m ()
|
||||
withLog action = do
|
||||
env <- ask
|
||||
liftIO . mapM_ action $ storeLog (env :: Env)
|
||||
|
||||
randomId :: (MonadUnliftIO m, MonadReader Env m) => Int -> m Encoded
|
||||
randomId n = do
|
||||
gVar <- asks idsDrg
|
||||
atomically (randomBytes n gVar)
|
||||
|
||||
randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString
|
||||
randomBytes n gVar = do
|
||||
g <- readTVar gVar
|
||||
let (bytes, g') = randomBytesGenerate n g
|
||||
writeTVar gVar g'
|
||||
return bytes
|
||||
@@ -0,0 +1,97 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Server.Env.STM where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Network.Socket (ServiceName)
|
||||
import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.MsgStore.STM
|
||||
import Simplex.Messaging.Server.QueueStore (QueueRec (..))
|
||||
import Simplex.Messaging.Server.QueueStore.STM
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import System.IO (IOMode (..))
|
||||
import UnliftIO.STM
|
||||
|
||||
data ServerConfig = ServerConfig
|
||||
{ tcpPort :: ServiceName,
|
||||
tbqSize :: Natural,
|
||||
queueIdBytes :: Int,
|
||||
msgIdBytes :: Int,
|
||||
storeLog :: Maybe (StoreLog 'ReadMode),
|
||||
serverPrivateKey :: C.FullPrivateKey
|
||||
-- serverId :: ByteString
|
||||
}
|
||||
|
||||
data Env = Env
|
||||
{ config :: ServerConfig,
|
||||
server :: Server,
|
||||
queueStore :: QueueStore,
|
||||
msgStore :: STMMsgStore,
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
serverKeyPair :: C.FullKeyPair,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode)
|
||||
}
|
||||
|
||||
data Server = Server
|
||||
{ subscribedQ :: TBQueue (RecipientId, Client),
|
||||
subscribers :: TVar (Map RecipientId Client)
|
||||
}
|
||||
|
||||
data Client = Client
|
||||
{ subscriptions :: TVar (Map RecipientId Sub),
|
||||
rcvQ :: TBQueue Transmission,
|
||||
sndQ :: TBQueue Transmission
|
||||
}
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread ThreadId
|
||||
|
||||
data Sub = Sub
|
||||
{ subThread :: SubscriptionThread,
|
||||
delivered :: TMVar ()
|
||||
}
|
||||
|
||||
newServer :: Natural -> STM Server
|
||||
newServer qSize = do
|
||||
subscribedQ <- newTBQueue qSize
|
||||
subscribers <- newTVar M.empty
|
||||
return Server {subscribedQ, subscribers}
|
||||
|
||||
newClient :: Natural -> STM Client
|
||||
newClient qSize = do
|
||||
subscriptions <- newTVar M.empty
|
||||
rcvQ <- newTBQueue qSize
|
||||
sndQ <- newTBQueue qSize
|
||||
return Client {subscriptions, rcvQ, sndQ}
|
||||
|
||||
newSubscription :: STM Sub
|
||||
newSubscription = do
|
||||
delivered <- newEmptyTMVar
|
||||
return Sub {subThread = NoSub, delivered}
|
||||
|
||||
newEnv :: forall m. (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
|
||||
newEnv config = do
|
||||
server <- atomically $ newServer (tbqSize config)
|
||||
queueStore <- atomically newQueueStore
|
||||
msgStore <- atomically newMsgStore
|
||||
idsDrg <- drgNew >>= newTVarIO
|
||||
s' <- restoreQueues queueStore `mapM` storeLog (config :: ServerConfig)
|
||||
let pk = serverPrivateKey config
|
||||
serverKeyPair = (C.publicKey pk, pk)
|
||||
return Env {config, server, queueStore, msgStore, idsDrg, serverKeyPair, storeLog = s'}
|
||||
where
|
||||
restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode)
|
||||
restoreQueues queueStore s = do
|
||||
(queues, s') <- liftIO $ readWriteStoreLog s
|
||||
atomically $ modifyTVar queueStore $ \d -> d {queues, senders = M.foldr' addSender M.empty queues}
|
||||
pure s'
|
||||
addSender :: QueueRec -> Map SenderId RecipientId -> Map SenderId RecipientId
|
||||
addSender q = M.insert (senderId q) (recipientId q)
|
||||
@@ -1,9 +1,9 @@
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
|
||||
module MsgStore where
|
||||
module Simplex.Messaging.Server.MsgStore where
|
||||
|
||||
import Data.Time.Clock
|
||||
import Transmission
|
||||
import Simplex.Messaging.Protocol (Encoded, MsgBody, RecipientId)
|
||||
|
||||
data Message = Message
|
||||
{ msgId :: Encoded,
|
||||
@@ -4,12 +4,12 @@
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
|
||||
module MsgStore.STM where
|
||||
module Simplex.Messaging.Server.MsgStore.STM where
|
||||
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import MsgStore
|
||||
import Transmission
|
||||
import Simplex.Messaging.Protocol (RecipientId)
|
||||
import Simplex.Messaging.Server.MsgStore
|
||||
import UnliftIO.STM
|
||||
|
||||
newtype MsgQueue = MsgQueue {msgQueue :: TQueue Message}
|
||||
@@ -3,28 +3,28 @@
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module QueueStore where
|
||||
module Simplex.Messaging.Server.QueueStore where
|
||||
|
||||
import Transmission
|
||||
import Simplex.Messaging.Protocol
|
||||
|
||||
data QueueRec = QueueRec
|
||||
{ recipientId :: QueueId,
|
||||
senderId :: QueueId,
|
||||
recipientKey :: PublicKey,
|
||||
senderKey :: Maybe PublicKey,
|
||||
recipientKey :: RecipientPublicKey,
|
||||
senderKey :: Maybe SenderPublicKey,
|
||||
status :: QueueStatus
|
||||
}
|
||||
|
||||
data QueueStatus = QueueActive | QueueOff
|
||||
data QueueStatus = QueueActive | QueueOff deriving (Eq)
|
||||
|
||||
class MonadQueueStore s m where
|
||||
addQueue :: s -> RecipientKey -> (RecipientId, SenderId) -> m (Either ErrorType ())
|
||||
addQueue :: s -> RecipientPublicKey -> (RecipientId, SenderId) -> m (Either ErrorType ())
|
||||
getQueue :: s -> SParty (a :: Party) -> QueueId -> m (Either ErrorType QueueRec)
|
||||
secureQueue :: s -> RecipientId -> SenderKey -> m (Either ErrorType ())
|
||||
secureQueue :: s -> RecipientId -> SenderPublicKey -> m (Either ErrorType ())
|
||||
suspendQueue :: s -> RecipientId -> m (Either ErrorType ())
|
||||
deleteQueue :: s -> RecipientId -> m (Either ErrorType ())
|
||||
|
||||
mkQueueRec :: RecipientKey -> (RecipientId, SenderId) -> QueueRec
|
||||
mkQueueRec :: RecipientPublicKey -> (RecipientId, SenderId) -> QueueRec
|
||||
mkQueueRec recipientKey (recipientId, senderId) =
|
||||
QueueRec
|
||||
{ recipientId,
|
||||
@@ -9,12 +9,12 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module QueueStore.STM where
|
||||
module Simplex.Messaging.Server.QueueStore.STM where
|
||||
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import QueueStore
|
||||
import Transmission
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.QueueStore
|
||||
import UnliftIO.STM
|
||||
|
||||
data QueueStoreData = QueueStoreData
|
||||
@@ -28,11 +28,11 @@ newQueueStore :: STM QueueStore
|
||||
newQueueStore = newTVar QueueStoreData {queues = M.empty, senders = M.empty}
|
||||
|
||||
instance MonadQueueStore QueueStore STM where
|
||||
addQueue :: QueueStore -> RecipientKey -> (RecipientId, SenderId) -> STM (Either ErrorType ())
|
||||
addQueue :: QueueStore -> RecipientPublicKey -> (RecipientId, SenderId) -> STM (Either ErrorType ())
|
||||
addQueue store rKey ids@(rId, sId) = do
|
||||
cs@QueueStoreData {queues, senders} <- readTVar store
|
||||
if M.member rId queues || M.member sId senders
|
||||
then return $ Left DUPLICATE
|
||||
then return $ Left DUPLICATE_
|
||||
else do
|
||||
writeTVar store $
|
||||
cs
|
||||
@@ -0,0 +1,140 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Server.StoreLog
|
||||
( StoreLog, -- constructors are not exported
|
||||
openWriteStoreLog,
|
||||
openReadStoreLog,
|
||||
closeStoreLog,
|
||||
logCreateQueue,
|
||||
logSecureQueue,
|
||||
logDeleteQueue,
|
||||
readWriteStoreLog,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Monad (unless)
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first, second)
|
||||
import Data.ByteString.Base64 (encode)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.List (foldl')
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers (base64P, parseAll)
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.QueueStore (QueueRec (..), QueueStatus (..))
|
||||
import Simplex.Messaging.Transport (trimCR)
|
||||
import System.Directory (doesFileExist)
|
||||
import System.IO
|
||||
|
||||
-- | opaque container for file handle with a type-safe IOMode
|
||||
-- constructors are not exported, openWriteStoreLog and openReadStoreLog should be used instead
|
||||
data StoreLog (a :: IOMode) where
|
||||
ReadStoreLog :: FilePath -> Handle -> StoreLog 'ReadMode
|
||||
WriteStoreLog :: FilePath -> Handle -> StoreLog 'WriteMode
|
||||
|
||||
data StoreLogRecord
|
||||
= CreateQueue QueueRec
|
||||
| SecureQueue QueueId SenderPublicKey
|
||||
| DeleteQueue QueueId
|
||||
|
||||
storeLogRecordP :: Parser StoreLogRecord
|
||||
storeLogRecordP =
|
||||
"CREATE " *> createQueueP
|
||||
<|> "SECURE " *> secureQueueP
|
||||
<|> "DELETE " *> (DeleteQueue <$> base64P)
|
||||
where
|
||||
createQueueP = CreateQueue <$> queueRecP
|
||||
secureQueueP = SecureQueue <$> base64P <* A.space <*> C.pubKeyP
|
||||
queueRecP = do
|
||||
recipientId <- "rid=" *> base64P <* A.space
|
||||
senderId <- "sid=" *> base64P <* A.space
|
||||
recipientKey <- "rk=" *> C.pubKeyP <* A.space
|
||||
senderKey <- "sk=" *> optional C.pubKeyP
|
||||
pure QueueRec {recipientId, senderId, recipientKey, senderKey, status = QueueActive}
|
||||
|
||||
serializeStoreLogRecord :: StoreLogRecord -> ByteString
|
||||
serializeStoreLogRecord = \case
|
||||
CreateQueue q -> "CREATE " <> serializeQueue q
|
||||
SecureQueue rId sKey -> "SECURE " <> encode rId <> " " <> C.serializePubKey sKey
|
||||
DeleteQueue rId -> "DELETE " <> encode rId
|
||||
where
|
||||
serializeQueue QueueRec {recipientId, senderId, recipientKey, senderKey} =
|
||||
B.unwords
|
||||
[ "rid=" <> encode recipientId,
|
||||
"sid=" <> encode senderId,
|
||||
"rk=" <> C.serializePubKey recipientKey,
|
||||
"sk=" <> maybe "" C.serializePubKey senderKey
|
||||
]
|
||||
|
||||
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
|
||||
openWriteStoreLog f = WriteStoreLog f <$> openFile f WriteMode
|
||||
|
||||
openReadStoreLog :: FilePath -> IO (StoreLog 'ReadMode)
|
||||
openReadStoreLog f = do
|
||||
doesFileExist f >>= (`unless` writeFile f "")
|
||||
ReadStoreLog f <$> openFile f ReadMode
|
||||
|
||||
closeStoreLog :: StoreLog a -> IO ()
|
||||
closeStoreLog = \case
|
||||
WriteStoreLog _ h -> hClose h
|
||||
ReadStoreLog _ h -> hClose h
|
||||
|
||||
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
|
||||
writeStoreLogRecord (WriteStoreLog _ h) r = do
|
||||
B.hPutStrLn h $ serializeStoreLogRecord r
|
||||
hFlush h
|
||||
|
||||
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
|
||||
logCreateQueue s = writeStoreLogRecord s . CreateQueue
|
||||
|
||||
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SenderPublicKey -> IO ()
|
||||
logSecureQueue s qId sKey = writeStoreLogRecord s $ SecureQueue qId sKey
|
||||
|
||||
logDeleteQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
|
||||
logDeleteQueue s = writeStoreLogRecord s . DeleteQueue
|
||||
|
||||
readWriteStoreLog :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec, StoreLog 'WriteMode)
|
||||
readWriteStoreLog s@(ReadStoreLog f _) = do
|
||||
qs <- readQueues s
|
||||
closeStoreLog s
|
||||
s' <- openWriteStoreLog f
|
||||
writeQueues s' qs
|
||||
pure (qs, s')
|
||||
|
||||
writeQueues :: StoreLog 'WriteMode -> Map RecipientId QueueRec -> IO ()
|
||||
writeQueues s = mapM_ (writeStoreLogRecord s . CreateQueue) . M.filter active
|
||||
where
|
||||
active QueueRec {status} = status == QueueActive
|
||||
|
||||
type LogParsingError = (String, ByteString)
|
||||
|
||||
readQueues :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec)
|
||||
readQueues (ReadStoreLog _ h) = LB.hGetContents h >>= returnResult . procStoreLog
|
||||
where
|
||||
procStoreLog :: LB.ByteString -> ([LogParsingError], Map RecipientId QueueRec)
|
||||
procStoreLog = second (foldl' procLogRecord M.empty) . partitionEithers . map parseLogRecord . LB.lines
|
||||
returnResult :: ([LogParsingError], Map RecipientId QueueRec) -> IO (Map RecipientId QueueRec)
|
||||
returnResult (errs, res) = mapM_ printError errs $> res
|
||||
parseLogRecord :: LB.ByteString -> Either LogParsingError StoreLogRecord
|
||||
parseLogRecord = (\s -> first (,s) $ parseAll storeLogRecordP s) . trimCR . LB.toStrict
|
||||
procLogRecord :: Map RecipientId QueueRec -> StoreLogRecord -> Map RecipientId QueueRec
|
||||
procLogRecord m = \case
|
||||
CreateQueue q -> M.insert (recipientId q) q m
|
||||
SecureQueue qId sKey -> M.adjust (\q -> q {senderKey = Just sKey}) qId m
|
||||
DeleteQueue qId -> M.delete qId m
|
||||
printError :: LogParsingError -> IO ()
|
||||
printError (e, s) = B.putStrLn $ "Error parsing log: " <> B.pack e <> " - " <> s
|
||||
@@ -0,0 +1,378 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Transport where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Trans.Except (throwE)
|
||||
import Crypto.Cipher.Types (AuthTag)
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteArray (xor)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Word (Word32)
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.IO.Exception (IOErrorType (..))
|
||||
import GHC.IO.Handle.Internals (ioe_EOF)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Network.Socket
|
||||
import Network.Transport.Internal (decodeNum16, decodeNum32, encodeEnum16, encodeEnum32, encodeWord32)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers (parse, parseAll, parseRead1)
|
||||
import Simplex.Messaging.Util (bshow, liftError)
|
||||
import System.IO
|
||||
import System.IO.Error
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Exception (Exception, IOException)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import qualified UnliftIO.IO as IO
|
||||
import UnliftIO.STM
|
||||
|
||||
-- * TCP transport
|
||||
|
||||
runTCPServer :: MonadUnliftIO m => TMVar Bool -> ServiceName -> (Handle -> m ()) -> m ()
|
||||
runTCPServer started port server = do
|
||||
clients <- newTVarIO S.empty
|
||||
E.bracket (liftIO $ startTCPServer started port) (liftIO . closeServer clients) \sock -> forever $ do
|
||||
h <- liftIO $ acceptTCPConn sock
|
||||
tid <- forkFinally (server h) (const $ IO.hClose h)
|
||||
atomically . modifyTVar clients $ S.insert tid
|
||||
where
|
||||
closeServer :: TVar (Set ThreadId) -> Socket -> IO ()
|
||||
closeServer clients sock = do
|
||||
readTVarIO clients >>= mapM_ killThread
|
||||
close sock
|
||||
void . atomically $ tryPutTMVar started False
|
||||
|
||||
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
|
||||
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
|
||||
where
|
||||
resolve =
|
||||
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
|
||||
in head <$> getAddrInfo (Just hints) Nothing (Just port)
|
||||
open addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
setSocketOption sock ReuseAddr 1
|
||||
withFdSocket sock setCloseOnExecIfNeeded
|
||||
bind sock $ addrAddress addr
|
||||
listen sock 1024
|
||||
return sock
|
||||
setStarted sock = atomically (putTMVar started True) >> pure sock
|
||||
|
||||
acceptTCPConn :: Socket -> IO Handle
|
||||
acceptTCPConn sock = accept sock >>= getSocketHandle . fst
|
||||
|
||||
runTCPClient :: MonadUnliftIO m => HostName -> ServiceName -> (Handle -> m a) -> m a
|
||||
runTCPClient host port client = do
|
||||
h <- liftIO $ startTCPClient host port
|
||||
client h `E.finally` IO.hClose h
|
||||
|
||||
startTCPClient :: HostName -> ServiceName -> IO Handle
|
||||
startTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
|
||||
where
|
||||
err :: IOException
|
||||
err = mkIOError NoSuchThing "no address" Nothing Nothing
|
||||
|
||||
resolve :: IO [AddrInfo]
|
||||
resolve =
|
||||
let hints = defaultHints {addrSocketType = Stream}
|
||||
in getAddrInfo (Just hints) (Just host) (Just port)
|
||||
|
||||
tryOpen :: IOException -> [AddrInfo] -> IO Handle
|
||||
tryOpen e [] = E.throwIO e
|
||||
tryOpen _ (addr : as) =
|
||||
E.try (open addr) >>= either (`tryOpen` as) pure
|
||||
|
||||
open :: AddrInfo -> IO Handle
|
||||
open addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
connect sock $ addrAddress addr
|
||||
getSocketHandle sock
|
||||
|
||||
getSocketHandle :: Socket -> IO Handle
|
||||
getSocketHandle conn = do
|
||||
h <- socketToHandle conn ReadWriteMode
|
||||
hSetBinaryMode h True
|
||||
hSetNewlineMode h NewlineMode {inputNL = CRLF, outputNL = CRLF}
|
||||
hSetBuffering h LineBuffering
|
||||
return h
|
||||
|
||||
putLn :: Handle -> ByteString -> IO ()
|
||||
putLn h = B.hPut h . (<> "\r\n")
|
||||
|
||||
getLn :: Handle -> IO ByteString
|
||||
getLn h = trimCR <$> B.hGetLine h
|
||||
|
||||
trimCR :: ByteString -> ByteString
|
||||
trimCR "" = ""
|
||||
trimCR s = if B.last s == '\r' then B.init s else s
|
||||
|
||||
-- * Encrypted transport
|
||||
|
||||
data SMPVersion = SMPVersion Int Int Int Int
|
||||
deriving (Eq, Ord)
|
||||
|
||||
major :: SMPVersion -> (Int, Int)
|
||||
major (SMPVersion a b _ _) = (a, b)
|
||||
|
||||
currentSMPVersion :: SMPVersion
|
||||
currentSMPVersion = SMPVersion 0 3 0 0
|
||||
|
||||
serializeSMPVersion :: SMPVersion -> ByteString
|
||||
serializeSMPVersion (SMPVersion a b c d) = B.intercalate "." [bshow a, bshow b, bshow c, bshow d]
|
||||
|
||||
smpVersionP :: Parser SMPVersion
|
||||
smpVersionP =
|
||||
let ver = A.decimal <* A.char '.'
|
||||
in SMPVersion <$> ver <*> ver <*> ver <*> A.decimal
|
||||
|
||||
data THandle = THandle
|
||||
{ handle :: Handle,
|
||||
sndKey :: SessionKey,
|
||||
rcvKey :: SessionKey,
|
||||
blockSize :: Int
|
||||
}
|
||||
|
||||
data SessionKey = SessionKey
|
||||
{ aesKey :: C.Key,
|
||||
baseIV :: C.IV,
|
||||
counter :: TVar Word32
|
||||
}
|
||||
|
||||
data ClientHandshake = ClientHandshake
|
||||
{ blockSize :: Int,
|
||||
sndKey :: SessionKey,
|
||||
rcvKey :: SessionKey
|
||||
}
|
||||
|
||||
data TransportError
|
||||
= TEBadBlock
|
||||
| TEEncrypt
|
||||
| TEDecrypt
|
||||
| TEHandshake HandshakeError
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
data HandshakeError
|
||||
= ENCRYPT
|
||||
| DECRYPT
|
||||
| VERSION
|
||||
| RSA_KEY
|
||||
| HEADER
|
||||
| AES_KEYS
|
||||
| BAD_HASH
|
||||
| MAJOR_VERSION
|
||||
| TERMINATED
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance Arbitrary TransportError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
|
||||
|
||||
transportErrorP :: Parser TransportError
|
||||
transportErrorP =
|
||||
"BLOCK" $> TEBadBlock
|
||||
<|> "AES_ENCRYPT" $> TEEncrypt
|
||||
<|> "AES_DECRYPT" $> TEDecrypt
|
||||
<|> TEHandshake <$> parseRead1
|
||||
|
||||
serializeTransportError :: TransportError -> ByteString
|
||||
serializeTransportError = \case
|
||||
TEEncrypt -> "AES_ENCRYPT"
|
||||
TEDecrypt -> "AES_DECRYPT"
|
||||
TEBadBlock -> "BLOCK"
|
||||
TEHandshake e -> bshow e
|
||||
|
||||
tPutEncrypted :: THandle -> ByteString -> IO (Either TransportError ())
|
||||
tPutEncrypted THandle {handle = h, sndKey, blockSize} block =
|
||||
encryptBlock sndKey (blockSize - C.authTagSize) block >>= \case
|
||||
Left _ -> pure $ Left TEEncrypt
|
||||
Right (authTag, msg) -> Right <$> B.hPut h (C.authTagToBS authTag <> msg)
|
||||
|
||||
tGetEncrypted :: THandle -> IO (Either TransportError ByteString)
|
||||
tGetEncrypted THandle {handle = h, rcvKey, blockSize} =
|
||||
B.hGet h blockSize >>= decryptBlock rcvKey >>= \case
|
||||
Left _ -> pure $ Left TEDecrypt
|
||||
Right "" -> ioe_EOF
|
||||
Right msg -> pure $ Right msg
|
||||
|
||||
encryptBlock :: SessionKey -> Int -> ByteString -> IO (Either C.CryptoError (AuthTag, ByteString))
|
||||
encryptBlock k@SessionKey {aesKey} size block = do
|
||||
ivBytes <- makeNextIV k
|
||||
runExceptT $ C.encryptAES aesKey ivBytes size block
|
||||
|
||||
decryptBlock :: SessionKey -> ByteString -> IO (Either C.CryptoError ByteString)
|
||||
decryptBlock k@SessionKey {aesKey} block = do
|
||||
let (authTag, msg') = B.splitAt C.authTagSize block
|
||||
ivBytes <- makeNextIV k
|
||||
runExceptT $ C.decryptAES aesKey ivBytes msg' (C.bsToAuthTag authTag)
|
||||
|
||||
makeNextIV :: SessionKey -> IO C.IV
|
||||
makeNextIV SessionKey {baseIV, counter} = atomically $ do
|
||||
c <- readTVar counter
|
||||
writeTVar counter $ c + 1
|
||||
pure $ iv c
|
||||
where
|
||||
(start, rest) = B.splitAt 4 $ C.unIV baseIV
|
||||
iv c = C.IV $ (start `xor` encodeWord32 c) <> rest
|
||||
|
||||
-- | implements server transport handshake as per /rfcs/2021-01-26-crypto.md#transport-encryption
|
||||
-- The numbers in function names refer to the steps in the document
|
||||
serverHandshake :: Handle -> C.FullKeyPair -> ExceptT TransportError IO THandle
|
||||
serverHandshake h (k, pk) = do
|
||||
liftIO sendHeaderAndPublicKey_1
|
||||
encryptedKeys <- receiveEncryptedKeys_4
|
||||
-- TODO server currently ignores blockSize returned by the client
|
||||
-- this is reserved for future support of streams
|
||||
ClientHandshake {blockSize = _, sndKey, rcvKey} <- decryptParseKeys_5 encryptedKeys
|
||||
th <- liftIO $ transportHandle h rcvKey sndKey transportBlockSize -- keys are swapped here
|
||||
sendWelcome_6 th
|
||||
pure th
|
||||
where
|
||||
sendHeaderAndPublicKey_1 :: IO ()
|
||||
sendHeaderAndPublicKey_1 = do
|
||||
let sKey = C.encodePubKey k
|
||||
header = ServerHeader {blockSize = transportBlockSize, keySize = B.length sKey}
|
||||
B.hPut h $ binaryServerHeader header <> sKey
|
||||
receiveEncryptedKeys_4 :: ExceptT TransportError IO ByteString
|
||||
receiveEncryptedKeys_4 =
|
||||
liftIO (B.hGet h $ C.publicKeySize k) >>= \case
|
||||
"" -> throwE $ TEHandshake TERMINATED
|
||||
ks -> pure ks
|
||||
decryptParseKeys_5 :: ByteString -> ExceptT TransportError IO ClientHandshake
|
||||
decryptParseKeys_5 encKeys =
|
||||
liftError (const $ TEHandshake DECRYPT) (C.decryptOAEP pk encKeys)
|
||||
>>= liftEither . parseClientHandshake
|
||||
sendWelcome_6 :: THandle -> ExceptT TransportError IO ()
|
||||
sendWelcome_6 th = ExceptT . tPutEncrypted th $ serializeSMPVersion currentSMPVersion <> " "
|
||||
|
||||
-- | implements client transport handshake as per /rfcs/2021-01-26-crypto.md#transport-encryption
|
||||
-- The numbers in function names refer to the steps in the document
|
||||
clientHandshake :: Handle -> Maybe C.KeyHash -> ExceptT TransportError IO THandle
|
||||
clientHandshake h keyHash = do
|
||||
(k, blkSize) <- getHeaderAndPublicKey_1_2
|
||||
-- TODO currently client always uses the blkSize returned by the server
|
||||
keys@ClientHandshake {sndKey, rcvKey} <- liftIO $ generateKeys_3 blkSize
|
||||
sendEncryptedKeys_4 k keys
|
||||
th <- liftIO $ transportHandle h sndKey rcvKey blkSize
|
||||
getWelcome_6 th >>= checkVersion
|
||||
pure th
|
||||
where
|
||||
getHeaderAndPublicKey_1_2 :: ExceptT TransportError IO (C.PublicKey, Int)
|
||||
getHeaderAndPublicKey_1_2 = do
|
||||
header <- liftIO (B.hGet h serverHeaderSize)
|
||||
ServerHeader {blockSize, keySize} <- liftEither $ parse serverHeaderP (TEHandshake HEADER) header
|
||||
when (blockSize < transportBlockSize || blockSize > maxTransportBlockSize) $
|
||||
throwError $ TEHandshake HEADER
|
||||
s <- liftIO $ B.hGet h keySize
|
||||
maybe (pure ()) (validateKeyHash_2 s) keyHash
|
||||
key <- liftEither $ parseKey s
|
||||
pure (key, blockSize)
|
||||
parseKey :: ByteString -> Either TransportError C.PublicKey
|
||||
parseKey = first (const $ TEHandshake RSA_KEY) . parseAll C.binaryPubKeyP
|
||||
validateKeyHash_2 :: ByteString -> C.KeyHash -> ExceptT TransportError IO ()
|
||||
validateKeyHash_2 k (C.KeyHash kHash)
|
||||
| C.sha256Hash k == kHash = pure ()
|
||||
| otherwise = throwE $ TEHandshake BAD_HASH
|
||||
generateKeys_3 :: Int -> IO ClientHandshake
|
||||
generateKeys_3 blkSize = ClientHandshake blkSize <$> generateKey <*> generateKey
|
||||
generateKey :: IO SessionKey
|
||||
generateKey = do
|
||||
aesKey <- C.randomAesKey
|
||||
baseIV <- C.randomIV
|
||||
pure SessionKey {aesKey, baseIV, counter = undefined}
|
||||
sendEncryptedKeys_4 :: C.PublicKey -> ClientHandshake -> ExceptT TransportError IO ()
|
||||
sendEncryptedKeys_4 k keys =
|
||||
liftError (const $ TEHandshake ENCRYPT) (C.encryptOAEP k $ serializeClientHandshake keys)
|
||||
>>= liftIO . B.hPut h
|
||||
getWelcome_6 :: THandle -> ExceptT TransportError IO SMPVersion
|
||||
getWelcome_6 th = ExceptT $ (>>= parseSMPVersion) <$> tGetEncrypted th
|
||||
parseSMPVersion :: ByteString -> Either TransportError SMPVersion
|
||||
parseSMPVersion = first (const $ TEHandshake VERSION) . A.parseOnly (smpVersionP <* A.space)
|
||||
checkVersion :: SMPVersion -> ExceptT TransportError IO ()
|
||||
checkVersion smpVersion =
|
||||
when (major smpVersion > major currentSMPVersion) . throwE $
|
||||
TEHandshake MAJOR_VERSION
|
||||
|
||||
data ServerHeader = ServerHeader {blockSize :: Int, keySize :: Int}
|
||||
deriving (Eq, Show)
|
||||
|
||||
binaryRsaTransport :: Int
|
||||
binaryRsaTransport = 0
|
||||
|
||||
transportBlockSize :: Int
|
||||
transportBlockSize = 4096
|
||||
|
||||
maxTransportBlockSize :: Int
|
||||
maxTransportBlockSize = 65536
|
||||
|
||||
serverHeaderSize :: Int
|
||||
serverHeaderSize = 8
|
||||
|
||||
binaryServerHeader :: ServerHeader -> ByteString
|
||||
binaryServerHeader ServerHeader {blockSize, keySize} =
|
||||
encodeEnum32 blockSize <> encodeEnum16 binaryRsaTransport <> encodeEnum16 keySize
|
||||
|
||||
serverHeaderP :: Parser ServerHeader
|
||||
serverHeaderP = ServerHeader <$> int32 <* binaryRsaTransportP <*> int16
|
||||
|
||||
serializeClientHandshake :: ClientHandshake -> ByteString
|
||||
serializeClientHandshake ClientHandshake {blockSize, sndKey, rcvKey} =
|
||||
encodeEnum32 blockSize <> encodeEnum16 binaryRsaTransport <> serializeKey sndKey <> serializeKey rcvKey
|
||||
where
|
||||
serializeKey :: SessionKey -> ByteString
|
||||
serializeKey SessionKey {aesKey, baseIV} = C.unKey aesKey <> C.unIV baseIV
|
||||
|
||||
clientHandshakeP :: Parser ClientHandshake
|
||||
clientHandshakeP = ClientHandshake <$> int32 <* binaryRsaTransportP <*> keyP <*> keyP
|
||||
where
|
||||
keyP :: Parser SessionKey
|
||||
keyP = do
|
||||
aesKey <- C.aesKeyP
|
||||
baseIV <- C.ivP
|
||||
pure SessionKey {aesKey, baseIV, counter = undefined}
|
||||
|
||||
int32 :: Parser Int
|
||||
int32 = decodeNum32 <$> A.take 4
|
||||
|
||||
int16 :: Parser Int
|
||||
int16 = decodeNum16 <$> A.take 2
|
||||
|
||||
binaryRsaTransportP :: Parser ()
|
||||
binaryRsaTransportP = binaryRsa =<< int16
|
||||
where
|
||||
binaryRsa :: Int -> Parser ()
|
||||
binaryRsa n
|
||||
| n == binaryRsaTransport = pure ()
|
||||
| otherwise = fail "unknown transport mode"
|
||||
|
||||
parseClientHandshake :: ByteString -> Either TransportError ClientHandshake
|
||||
parseClientHandshake = parse clientHandshakeP $ TEHandshake AES_KEYS
|
||||
|
||||
transportHandle :: Handle -> SessionKey -> SessionKey -> Int -> IO THandle
|
||||
transportHandle h sk rk blockSize = do
|
||||
sndCounter <- newTVarIO 0
|
||||
rcvCounter <- newTVarIO 0
|
||||
pure
|
||||
THandle
|
||||
{ handle = h,
|
||||
sndKey = sk {counter = sndCounter},
|
||||
rcvKey = rk {counter = rcvCounter},
|
||||
blockSize
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Simplex.Messaging.Util where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import UnliftIO.Async
|
||||
import UnliftIO.Exception (Exception)
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
newtype InternalException e = InternalException {unInternalException :: e}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Exception e => Exception (InternalException e)
|
||||
|
||||
instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
|
||||
withRunInIO :: ((forall a. ExceptT e m a -> IO a) -> IO b) -> ExceptT e m b
|
||||
withRunInIO exceptToIO =
|
||||
withExceptT unInternalException . ExceptT . E.try $
|
||||
withRunInIO $ \run ->
|
||||
exceptToIO $ run . (either (E.throwIO . InternalException) return <=< runExceptT)
|
||||
|
||||
raceAny_ :: MonadUnliftIO m => [m a] -> m ()
|
||||
raceAny_ = r []
|
||||
where
|
||||
r as (m : ms) = withAsync m $ \a -> r (a : as) ms
|
||||
r as [] = void $ waitAnyCancel as
|
||||
|
||||
infixl 4 <$$>, <$?>
|
||||
|
||||
(<$$>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
|
||||
(<$$>) = fmap . fmap
|
||||
|
||||
(<$?>) :: MonadFail m => (a -> Either String b) -> m a -> m b
|
||||
f <$?> m = m >>= either fail pure . f
|
||||
|
||||
bshow :: Show a => a -> ByteString
|
||||
bshow = B.pack . show
|
||||
|
||||
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither a = liftIO a >>= liftEither
|
||||
|
||||
liftError :: (MonadIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
|
||||
liftError f = liftEitherError f . runExceptT
|
||||
|
||||
liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
|
||||
liftEitherError f a = liftIOEither (first f <$> a)
|
||||
@@ -0,0 +1,99 @@
|
||||
# Errors
|
||||
|
||||
## Problems
|
||||
|
||||
- using numbers and strings to indicate errors (in protocol and in code) - ErrorType, AgentErrorType, TransportError
|
||||
- re-using the same type in multiple contexts (with some constructors not applicable to all contexts) - ErrorType
|
||||
|
||||
## Error types
|
||||
|
||||
### ErrorType (Protocol.hs)
|
||||
|
||||
- BLOCK - incorrect block format, encoding or signature size
|
||||
- CMD error - command is unknown or has invalid syntax, where `error` can be:
|
||||
- PROHIBITED - server response sent from client or vice versa
|
||||
- KEY_SIZE - bad RSA key size in NEW or KEY commands (only 1024, 2048 and 4096 bits keys are allowed)
|
||||
- SYNTAX - error parsing command
|
||||
- NO_AUTH - transmission has no required credentials (signature or queue ID)
|
||||
- HAS_AUTH - transmission has not allowed credentials
|
||||
- NO_QUEUE - transmission has not queue ID
|
||||
- AUTH - command is not authorised (queue does not exist or signature verification failed).
|
||||
- NO_MSG - acknowledging (ACK) the message without message
|
||||
- INTERNAL - internal server error.
|
||||
- DUPLICATE_ - it is used internally to signal that the queue ID is already used. This is NOT used in the protocol, instead INTERNAL is sent to the client. It has to be removed.
|
||||
|
||||
### AgentErrorType (Agent/Transmission.hs)
|
||||
|
||||
Some of these errors are not correctly serialized/parsed - see line 322 in Agent/Transmission.hs
|
||||
|
||||
- CMD e - command or response error
|
||||
- PROHIBITED - server response sent as client command (and vice versa)
|
||||
- SYNTAX - command is unknown or has invalid syntax.
|
||||
- NO_CONN - connection is required in the command (and absent)
|
||||
- SIZE - incorrect message size of messages (when parsing SEND and MSG)
|
||||
- LARGE -- message does not fit SMP block
|
||||
- CONN e - connection errors
|
||||
- UNKNOWN - connection alias not in database
|
||||
- DUPLICATE - connection alias already exists
|
||||
- SIMPLEX - connection is simplex, but operation requires another queue
|
||||
- SMP ErrorType - forwarding SMP errors (SMPServerError) to the agent client
|
||||
- BROKER e - SMP server errors
|
||||
- RESPONSE ErrorType - invalid SMP server response
|
||||
- UNEXPECTED - unexpected response
|
||||
- NETWORK - network TCP connection error
|
||||
- TRANSPORT TransportError -- handshake or other transport error
|
||||
- TIMEOUT - command response timeout
|
||||
- AGENT e - errors of other agents
|
||||
- A_MESSAGE - SMP message failed to parse
|
||||
- A_PROHIBITED - SMP message is prohibited with the current queue status
|
||||
- A_ENCRYPTION - cannot RSA/AES-decrypt or parse decrypted header
|
||||
- A_SIGNATURE - invalid RSA signature
|
||||
- INTERNAL ByteString - agent implementation or dependency error
|
||||
|
||||
### SMPClientError (Client.hs)
|
||||
|
||||
- SMPServerError ErrorType - this is correctly parsed server ERR response. This error is forwarded to the agent client as `ERR SMP err`
|
||||
- SMPResponseError ErrorType - this is invalid server response that failed to parse - forwarded to the client as `ERR BROKER RESPONSE`.
|
||||
- SMPUnexpectedResponse - different response from what is expected to a given command, e.g. server should respond `IDS` or `ERR` to `NEW` command, other responses would result in this error - forwarded to the client as `ERR BROKER UNEXPECTED`.
|
||||
- SMPResponseTimeout - used for TCP connection and command response timeouts -> `ERR BROKER TIMEOUT`.
|
||||
- SMPNetworkError - fails to establish TCP connection -> `ERR BROKER NETWORK`
|
||||
- SMPTransportError e - fails connection handshake or some other transport error -> `ERR BROKER TRANSPORT e`
|
||||
- SMPSignatureError C.CryptoError - error when cryptographically "signing" the command.
|
||||
|
||||
### StoreError (Agent/Store.hs)
|
||||
|
||||
- SEInternal ByteString - signals exceptions in store actions.
|
||||
- SEConnNotFound - connection alias not found (or both queues absent).
|
||||
- SEConnDuplicate - connection alias already used.
|
||||
- SEBadConnType ConnType - wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa. `updateRcvConnWithSndQueue` and `updateSndConnWithRcvQueue` do not allow duplex connections - they would also return this error.
|
||||
- SEBadQueueStatus - the intention was to pass current expected queue status in methods, as we always know what it should be at any stage of the protocol, and in case it does not match use this error. **Currently not used**.
|
||||
- SENotImplemented - used in `getMsg` that is not implemented/used.
|
||||
|
||||
### CryptoError (Crypto.hs)
|
||||
|
||||
- RSAEncryptError R.Error - RSA encryption error
|
||||
- RSADecryptError R.Error - RSA decryption error
|
||||
- RSASignError R.Error - RSA signature error
|
||||
- AESCipherError CE.CryptoError - AES initialization error
|
||||
- CryptoIVError - IV generation error
|
||||
- AESDecryptError - AES decryption error
|
||||
- CryptoLargeMsgError - message does not fit in SMP block
|
||||
- CryptoHeaderError String - failure parsing RSA-encrypted message header
|
||||
|
||||
### TransportError (Transport.hs)
|
||||
|
||||
- TEBadBlock - error parsing block
|
||||
- TEEncrypt - block encryption error
|
||||
- TEDecrypt - block decryption error
|
||||
- TEHandshake HandshakeError
|
||||
|
||||
### HandshakeError (Transport.hs)
|
||||
|
||||
- ENCRYPT - encryption error
|
||||
- DECRYPT - decryption error
|
||||
- VERSION - error parsing protocol version
|
||||
- RSA_KEY - error parsing RSA key
|
||||
- AES_KEYS - error parsing AES keys
|
||||
- BAD_HASH - not matching RSA key hash
|
||||
- MAJOR_VERSION - lower agent version than protocol version
|
||||
- TERMINATED - transport terminated
|
||||
@@ -1,176 +0,0 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Transmission where
|
||||
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (ord)
|
||||
import Data.Kind
|
||||
import Data.Time.Clock
|
||||
import Data.Time.ISO8601
|
||||
|
||||
data Party = Broker | Recipient | Sender
|
||||
deriving (Show)
|
||||
|
||||
data SParty :: Party -> Type where
|
||||
SBroker :: SParty Broker
|
||||
SRecipient :: SParty Recipient
|
||||
SSender :: SParty Sender
|
||||
|
||||
deriving instance Show (SParty a)
|
||||
|
||||
data Cmd where
|
||||
Cmd :: SParty a -> Command a -> Cmd
|
||||
|
||||
deriving instance Show Cmd
|
||||
|
||||
type Signed = (QueueId, Cmd)
|
||||
|
||||
type Transmission = (Signature, Signed)
|
||||
|
||||
type SignedOrError = (QueueId, Either ErrorType Cmd)
|
||||
|
||||
type TransmissionOrError = (Signature, SignedOrError)
|
||||
|
||||
type RawTransmission = (ByteString, ByteString, ByteString)
|
||||
|
||||
data Command (a :: Party) where
|
||||
NEW :: RecipientKey -> Command Recipient
|
||||
SUB :: Command Recipient
|
||||
KEY :: SenderKey -> Command Recipient
|
||||
ACK :: Command Recipient
|
||||
OFF :: Command Recipient
|
||||
DEL :: Command Recipient
|
||||
SEND :: MsgBody -> Command Sender
|
||||
IDS :: RecipientId -> SenderId -> Command Broker
|
||||
MSG :: MsgId -> UTCTime -> MsgBody -> Command Broker
|
||||
END :: Command Broker
|
||||
OK :: Command Broker
|
||||
ERR :: ErrorType -> Command Broker
|
||||
|
||||
deriving instance Show (Command a)
|
||||
|
||||
deriving instance Eq (Command a)
|
||||
|
||||
parseCommand :: ByteString -> Either ErrorType Cmd
|
||||
parseCommand command = case B.words command of
|
||||
["NEW", rKeyStr] -> case decode rKeyStr of
|
||||
Right rKey -> rCmd $ NEW rKey
|
||||
_ -> errParams
|
||||
["SUB"] -> rCmd SUB
|
||||
["KEY", sKeyStr] -> case decode sKeyStr of
|
||||
Right sKey -> rCmd $ KEY sKey
|
||||
_ -> errParams
|
||||
["ACK"] -> rCmd ACK
|
||||
["OFF"] -> rCmd OFF
|
||||
["DEL"] -> rCmd DEL
|
||||
["SEND"] -> errParams
|
||||
"SEND" : msgBody -> Right . Cmd SSender . SEND $ B.unwords msgBody
|
||||
["IDS", rIdStr, sIdStr] -> case decode rIdStr of
|
||||
Right rId -> case decode sIdStr of
|
||||
Right sId -> bCmd $ IDS rId sId
|
||||
_ -> errParams
|
||||
_ -> errParams
|
||||
["MSG", msgIdStr, ts, msgBody] -> case decode msgIdStr of
|
||||
Right msgId -> case parseISO8601 $ B.unpack ts of
|
||||
Just utc -> bCmd $ MSG msgId utc msgBody
|
||||
_ -> errParams
|
||||
_ -> errParams
|
||||
["END"] -> bCmd END
|
||||
["OK"] -> bCmd OK
|
||||
"ERR" : err -> case err of
|
||||
["UNKNOWN"] -> bErr UNKNOWN
|
||||
["PROHIBITED"] -> bErr PROHIBITED
|
||||
["SYNTAX", errCode] -> maybe errParams (bErr . SYNTAX) $ digitToInt $ B.unpack errCode
|
||||
["SIZE"] -> bErr SIZE
|
||||
["AUTH"] -> bErr AUTH
|
||||
["INTERNAL"] -> bErr INTERNAL
|
||||
_ -> errParams
|
||||
"NEW" : _ -> errParams
|
||||
"SUB" : _ -> errParams
|
||||
"KEY" : _ -> errParams
|
||||
"ACK" : _ -> errParams
|
||||
"OFF" : _ -> errParams
|
||||
"DEL" : _ -> errParams
|
||||
"MSG" : _ -> errParams
|
||||
"IDS" : _ -> errParams
|
||||
"END" : _ -> errParams
|
||||
"OK" : _ -> errParams
|
||||
_ -> Left UNKNOWN
|
||||
where
|
||||
errParams = Left $ SYNTAX errBadParameters
|
||||
rCmd = Right . Cmd SRecipient
|
||||
bCmd = Right . Cmd SBroker
|
||||
bErr = bCmd . ERR
|
||||
|
||||
digitToInt :: String -> Maybe Int
|
||||
digitToInt [c] =
|
||||
let i = ord c - zero
|
||||
in if i >= 0 && i <= 9 then Just i else Nothing
|
||||
digitToInt _ = Nothing
|
||||
|
||||
zero :: Int
|
||||
zero = ord '0'
|
||||
|
||||
serializeCommand :: Cmd -> ByteString
|
||||
serializeCommand = \case
|
||||
Cmd SRecipient (NEW rKey) -> "NEW " <> encode rKey
|
||||
Cmd SRecipient (KEY sKey) -> "KEY " <> encode sKey
|
||||
Cmd SRecipient cmd -> B.pack $ show cmd
|
||||
Cmd SSender (SEND msgBody) -> "SEND" <> serializeMsg msgBody
|
||||
Cmd SBroker (MSG msgId ts msgBody) ->
|
||||
B.unwords ["MSG", encode msgId, B.pack $ formatISO8601Millis ts] <> serializeMsg msgBody
|
||||
Cmd SBroker (IDS rId sId) -> B.unwords ["IDS", encode rId, encode sId]
|
||||
Cmd SBroker (ERR err) -> "ERR " <> B.pack (show err)
|
||||
Cmd SBroker resp -> B.pack $ show resp
|
||||
where
|
||||
serializeMsg msgBody = " " <> B.pack (show $ B.length msgBody) <> "\n" <> msgBody
|
||||
|
||||
type Encoded = ByteString
|
||||
|
||||
type PublicKey = Encoded
|
||||
|
||||
type Signature = Encoded
|
||||
|
||||
type RecipientKey = PublicKey
|
||||
|
||||
type SenderKey = PublicKey
|
||||
|
||||
type RecipientId = QueueId
|
||||
|
||||
type SenderId = QueueId
|
||||
|
||||
type QueueId = Encoded
|
||||
|
||||
type MsgId = Encoded
|
||||
|
||||
type MsgBody = ByteString
|
||||
|
||||
data ErrorType = UNKNOWN | PROHIBITED | SYNTAX Int | SIZE | AUTH | INTERNAL | DUPLICATE deriving (Show, Eq)
|
||||
|
||||
errBadTransmission :: Int
|
||||
errBadTransmission = 1
|
||||
|
||||
errBadParameters :: Int
|
||||
errBadParameters = 2
|
||||
|
||||
errNoCredentials :: Int
|
||||
errNoCredentials = 3
|
||||
|
||||
errHasCredentials :: Int
|
||||
errHasCredentials = 4
|
||||
|
||||
errNoConnectionId :: Int
|
||||
errNoConnectionId = 5
|
||||
|
||||
errMessageBody :: Int
|
||||
errMessageBody = 6
|
||||
@@ -1,167 +0,0 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Transport where
|
||||
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Network.Socket
|
||||
import System.IO
|
||||
import Text.Read
|
||||
import Transmission
|
||||
import UnliftIO.Concurrent
|
||||
import qualified UnliftIO.Exception as E
|
||||
import qualified UnliftIO.IO as IO
|
||||
|
||||
startTCPServer :: MonadIO m => ServiceName -> m Socket
|
||||
startTCPServer port = liftIO . withSocketsDo $ resolve >>= open
|
||||
where
|
||||
resolve = do
|
||||
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
|
||||
head <$> getAddrInfo (Just hints) Nothing (Just port)
|
||||
open addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
setSocketOption sock ReuseAddr 1
|
||||
withFdSocket sock setCloseOnExecIfNeeded
|
||||
bind sock $ addrAddress addr
|
||||
listen sock 1024
|
||||
return sock
|
||||
|
||||
runTCPServer :: MonadUnliftIO m => ServiceName -> (Handle -> m ()) -> m ()
|
||||
runTCPServer port server =
|
||||
E.bracket (startTCPServer port) (liftIO . close) $ \sock -> forever $ do
|
||||
h <- acceptTCPConn sock
|
||||
forkFinally (server h) (const $ IO.hClose h)
|
||||
|
||||
acceptTCPConn :: MonadIO m => Socket -> m Handle
|
||||
acceptTCPConn sock = liftIO $ do
|
||||
(conn, _) <- accept sock
|
||||
-- putStrLn $ "Accepted connection from " ++ show peer
|
||||
getSocketHandle conn
|
||||
|
||||
startTCPClient :: MonadIO m => HostName -> ServiceName -> m Handle
|
||||
startTCPClient host port = liftIO . withSocketsDo $ resolve >>= open
|
||||
where
|
||||
resolve = do
|
||||
let hints = defaultHints {addrSocketType = Stream}
|
||||
head <$> getAddrInfo (Just hints) (Just host) (Just port)
|
||||
open addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
connect sock $ addrAddress addr
|
||||
getSocketHandle sock
|
||||
|
||||
runTCPClient :: MonadUnliftIO m => HostName -> ServiceName -> (Handle -> m a) -> m a
|
||||
runTCPClient host port = E.bracket (startTCPClient host port) IO.hClose
|
||||
|
||||
getSocketHandle :: MonadIO m => Socket -> m Handle
|
||||
getSocketHandle conn = liftIO $ do
|
||||
h <- socketToHandle conn ReadWriteMode
|
||||
hSetBinaryMode h True
|
||||
hSetNewlineMode h NewlineMode {inputNL = CRLF, outputNL = CRLF}
|
||||
hSetBuffering h LineBuffering
|
||||
return h
|
||||
|
||||
putLn :: MonadIO m => Handle -> ByteString -> m ()
|
||||
putLn h = liftIO . hPutStrLn h . B.unpack
|
||||
|
||||
getLn :: MonadIO m => Handle -> m ByteString
|
||||
getLn h = B.pack <$> liftIO (hGetLine h)
|
||||
|
||||
getBytes :: MonadIO m => Handle -> Int -> m ByteString
|
||||
getBytes h = liftIO . B.hGet h
|
||||
|
||||
tPutRaw :: MonadIO m => Handle -> RawTransmission -> m ()
|
||||
tPutRaw h (signature, queueId, command) = do
|
||||
putLn h (encode signature)
|
||||
putLn h (encode queueId)
|
||||
putLn h command
|
||||
|
||||
tGetRaw :: MonadIO m => Handle -> m (Either String RawTransmission)
|
||||
tGetRaw h = do
|
||||
signature <- decode <$> getLn h
|
||||
queueId <- decode <$> getLn h
|
||||
command <- getLn h
|
||||
return $ liftM2 (,,command) signature queueId
|
||||
|
||||
tPut :: MonadIO m => Handle -> Transmission -> m ()
|
||||
tPut h (signature, (queueId, command)) = tPutRaw h (signature, queueId, serializeCommand command)
|
||||
|
||||
fromClient :: Cmd -> Either ErrorType Cmd
|
||||
fromClient = \case
|
||||
Cmd SBroker _ -> Left PROHIBITED
|
||||
cmd -> Right cmd
|
||||
|
||||
fromServer :: Cmd -> Either ErrorType Cmd
|
||||
fromServer = \case
|
||||
cmd@(Cmd SBroker _) -> Right cmd
|
||||
_ -> Left PROHIBITED
|
||||
|
||||
-- | get client and server transmissions
|
||||
-- `fromParty` is used to limit allowed senders - `fromClient` or `fromServer` should be used
|
||||
tGet :: forall m. MonadIO m => (Cmd -> Either ErrorType Cmd) -> Handle -> m TransmissionOrError
|
||||
tGet fromParty h = tGetRaw h >>= either (const tError) tParseLoadBody
|
||||
where
|
||||
tError :: m TransmissionOrError
|
||||
tError = return (B.empty, (B.empty, Left $ SYNTAX errBadTransmission))
|
||||
|
||||
tParseLoadBody :: RawTransmission -> m TransmissionOrError
|
||||
tParseLoadBody t@(signature, queueId, command) = do
|
||||
let cmd = parseCommand command >>= fromParty >>= tCredentials t
|
||||
fullCmd <- either (return . Left) cmdWithMsgBody cmd
|
||||
return (signature, (queueId, fullCmd))
|
||||
|
||||
tCredentials :: RawTransmission -> Cmd -> Either ErrorType Cmd
|
||||
tCredentials (signature, queueId, _) cmd = case cmd of
|
||||
-- IDS response should not have queue ID
|
||||
Cmd SBroker (IDS _ _) -> Right cmd
|
||||
-- ERROR response does not always have queue ID
|
||||
Cmd SBroker (ERR _) -> Right cmd
|
||||
-- other responses must have queue ID
|
||||
Cmd SBroker _
|
||||
| B.null queueId -> Left $ SYNTAX errNoConnectionId
|
||||
| otherwise -> Right cmd
|
||||
-- CREATE must NOT have signature or queue ID
|
||||
Cmd SRecipient (NEW _)
|
||||
| B.null signature && B.null queueId -> Right cmd
|
||||
| otherwise -> Left $ SYNTAX errHasCredentials
|
||||
-- SEND must have queue ID, signature is not always required
|
||||
Cmd SSender (SEND _)
|
||||
| B.null queueId -> Left $ SYNTAX errNoConnectionId
|
||||
| otherwise -> Right cmd
|
||||
-- other client commands must have both signature and queue ID
|
||||
Cmd SRecipient _
|
||||
| B.null signature || B.null queueId -> Left $ SYNTAX errNoCredentials
|
||||
| otherwise -> Right cmd
|
||||
|
||||
cmdWithMsgBody :: Cmd -> m (Either ErrorType Cmd)
|
||||
cmdWithMsgBody = \case
|
||||
Cmd SSender (SEND body) ->
|
||||
Cmd SSender . SEND <$$> getMsgBody body
|
||||
Cmd SBroker (MSG msgId ts body) ->
|
||||
Cmd SBroker . MSG msgId ts <$$> getMsgBody body
|
||||
cmd -> return $ Right cmd
|
||||
|
||||
getMsgBody :: MsgBody -> m (Either ErrorType MsgBody)
|
||||
getMsgBody msgBody =
|
||||
case B.unpack msgBody of
|
||||
':' : body -> return . Right $ B.pack body
|
||||
str -> case readMaybe str :: Maybe Int of
|
||||
Just size -> do
|
||||
body <- getBytes h size
|
||||
s <- getLn h
|
||||
return if B.null s then Right body else Left SIZE
|
||||
Nothing -> return . Left $ SYNTAX errMessageBody
|
||||
|
||||
infixl 4 <$$>
|
||||
|
||||
(<$$>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
|
||||
(<$$>) = fmap . fmap
|
||||
@@ -35,6 +35,10 @@ packages:
|
||||
# forks / in-progress versions pinned to a git hash. For example:
|
||||
#
|
||||
extra-deps:
|
||||
- cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881
|
||||
- direct-sqlite-2.3.26@sha256:04e835402f1508abca383182023e4e2b9b86297b8533afbd4e57d1a5652e0c23,3718
|
||||
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
|
||||
- sqlite-simple-0.4.18.0@sha256:3ceea56375c0a3590c814e411a4eb86943f8d31b93b110ca159c90689b6b39e5,3002
|
||||
# - network-run-0.2.4@sha256:7dbb06def522dab413bce4a46af476820bffdff2071974736b06f52f4ab57c96,885
|
||||
# - git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
module AgentTests where
|
||||
|
||||
import AgentTests.SQLiteTests (storeTests)
|
||||
import Control.Concurrent
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import SMPAgentClient
|
||||
import SMPClient (testKeyHashStr)
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
|
||||
import System.IO (Handle)
|
||||
import System.Timeout
|
||||
import Test.Hspec
|
||||
|
||||
agentTests :: Spec
|
||||
agentTests = do
|
||||
describe "SQLite store" storeTests
|
||||
describe "SMP agent protocol syntax" syntaxTests
|
||||
describe "Establishing duplex connection" do
|
||||
it "should connect via one server and one agent" $
|
||||
smpAgentTest2_1 testDuplexConnection
|
||||
it "should connect via one server and 2 agents" $
|
||||
smpAgentTest2 testDuplexConnection
|
||||
describe "Connection subscriptions" do
|
||||
xit "should connect via one server and one agent" $
|
||||
smpAgentTest3_1 testSubscription
|
||||
it "should send notifications to client when server disconnects" $
|
||||
smpAgentServerTest testSubscrNotification
|
||||
|
||||
-- | simple test for one command with the expected response
|
||||
(>#>) :: ARawTransmission -> ARawTransmission -> Expectation
|
||||
command >#> response = smpAgentTest command `shouldReturn` response
|
||||
|
||||
-- | simple test for one command with a predicate for the expected response
|
||||
(>#>=) :: ARawTransmission -> ((ByteString, ByteString, [ByteString]) -> Bool) -> Expectation
|
||||
command >#>= p = smpAgentTest command >>= (`shouldSatisfy` p . \(cId, cAlias, cmd) -> (cId, cAlias, B.words cmd))
|
||||
|
||||
-- | send transmission `t` to handle `h` and get response
|
||||
(#:) :: Handle -> (ByteString, ByteString, ByteString) -> IO (ATransmissionOrError 'Agent)
|
||||
h #: t = tPutRaw h t >> tGet SAgent h
|
||||
|
||||
-- | action and expected response
|
||||
-- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r`
|
||||
(#>) :: IO (ATransmissionOrError 'Agent) -> ATransmission 'Agent -> Expectation
|
||||
action #> (corrId, cAlias, cmd) = action `shouldReturn` (corrId, cAlias, Right cmd)
|
||||
|
||||
-- | action and predicate for the response
|
||||
-- `h #:t =#> p` is the test that sends `t` to `h` and validates the response using `p`
|
||||
(=#>) :: IO (ATransmissionOrError 'Agent) -> (ATransmission 'Agent -> Bool) -> Expectation
|
||||
action =#> p = action >>= (`shouldSatisfy` p . correctTransmission)
|
||||
|
||||
correctTransmission :: ATransmissionOrError a -> ATransmission a
|
||||
correctTransmission (corrId, cAlias, cmdOrErr) = case cmdOrErr of
|
||||
Right cmd -> (corrId, cAlias, cmd)
|
||||
Left e -> error $ show e
|
||||
|
||||
-- | receive message to handle `h` and validate that it is the expected one
|
||||
(<#) :: Handle -> ATransmission 'Agent -> Expectation
|
||||
h <# (corrId, cAlias, cmd) = tGet SAgent h `shouldReturn` (corrId, cAlias, Right cmd)
|
||||
|
||||
-- | receive message to handle `h` and validate it using predicate `p`
|
||||
(<#=) :: Handle -> (ATransmission 'Agent -> Bool) -> Expectation
|
||||
h <#= p = tGet SAgent h >>= (`shouldSatisfy` p . correctTransmission)
|
||||
|
||||
-- | test that nothing is delivered to handle `h` during 10ms
|
||||
(#:#) :: Handle -> String -> Expectation
|
||||
h #:# err = tryGet `shouldReturn` ()
|
||||
where
|
||||
tryGet =
|
||||
10000 `timeout` tGet SAgent h >>= \case
|
||||
Just _ -> error err
|
||||
_ -> return ()
|
||||
|
||||
pattern Msg :: MsgBody -> ACommand 'Agent
|
||||
pattern Msg msgBody <- MSG {msgBody, msgIntegrity = MsgOk}
|
||||
|
||||
testDuplexConnection :: Handle -> Handle -> IO ()
|
||||
testDuplexConnection alice bob = do
|
||||
("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW localhost:5000")
|
||||
let qInfo' = serializeSmpQueueInfo qInfo
|
||||
bob #: ("11", "alice", "JOIN " <> qInfo') #> ("11", "alice", CON)
|
||||
alice <# ("", "bob", CON)
|
||||
alice #: ("2", "bob", "SEND :hello") =#> \case ("2", "bob", SENT 1) -> True; _ -> False
|
||||
alice #: ("3", "bob", "SEND :how are you?") =#> \case ("3", "bob", SENT 2) -> True; _ -> False
|
||||
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
|
||||
bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False
|
||||
bob #: ("14", "alice", "SEND 9\nhello too") =#> \case ("14", "alice", SENT 3) -> True; _ -> False
|
||||
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
|
||||
bob #: ("15", "alice", "SEND 9\nmessage 1") =#> \case ("15", "alice", SENT 4) -> True; _ -> False
|
||||
alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False
|
||||
alice #: ("5", "bob", "OFF") #> ("5", "bob", OK)
|
||||
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", ERR (SMP AUTH))
|
||||
alice #: ("6", "bob", "DEL") #> ("6", "bob", OK)
|
||||
alice #:# "nothing else should be delivered to alice"
|
||||
|
||||
testSubscription :: Handle -> Handle -> Handle -> IO ()
|
||||
testSubscription alice1 alice2 bob = do
|
||||
("1", "bob", Right (INV qInfo)) <- alice1 #: ("1", "bob", "NEW localhost:5000")
|
||||
let qInfo' = serializeSmpQueueInfo qInfo
|
||||
bob #: ("11", "alice", "JOIN " <> qInfo') #> ("11", "alice", CON)
|
||||
bob #: ("12", "alice", "SEND 5\nhello") =#> \case ("12", "alice", SENT _) -> True; _ -> False
|
||||
bob #: ("13", "alice", "SEND 11\nhello again") =#> \case ("13", "alice", SENT _) -> True; _ -> False
|
||||
alice1 <# ("", "bob", CON)
|
||||
alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False
|
||||
-- alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False
|
||||
t <- tGet SAgent alice1
|
||||
print t
|
||||
t `shouldSatisfy` (\case ("", "bob", Msg "hello again") -> True; _ -> False) . correctTransmission
|
||||
alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK)
|
||||
alice1 <# ("", "bob", END)
|
||||
bob #: ("14", "alice", "SEND 2\nhi") =#> \case ("14", "alice", SENT _) -> True; _ -> False
|
||||
alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False
|
||||
alice1 #:# "nothing else should be delivered to alice1"
|
||||
|
||||
testSubscrNotification :: (ThreadId, ThreadId) -> Handle -> IO ()
|
||||
testSubscrNotification (server, _) client = do
|
||||
client #: ("1", "conn1", "NEW localhost:5000") =#> \case ("1", "conn1", INV _) -> True; _ -> False
|
||||
client #:# "nothing should be delivered to client before the server is killed"
|
||||
killThread server
|
||||
client <# ("", "conn1", END)
|
||||
|
||||
samplePublicKey :: ByteString
|
||||
samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR"
|
||||
|
||||
syntaxTests :: Spec
|
||||
syntaxTests = do
|
||||
it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX")
|
||||
describe "NEW" do
|
||||
describe "valid" do
|
||||
-- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided)
|
||||
-- TODO: add tests with defined connection alias
|
||||
xit "only server" $ ("211", "", "NEW localhost") >#>= \case ("211", "", "INV" : _) -> True; _ -> False
|
||||
it "with port" $ ("212", "", "NEW localhost:5000") >#>= \case ("212", "", "INV" : _) -> True; _ -> False
|
||||
xit "with keyHash" $ ("213", "", "NEW localhost#" <> testKeyHashStr) >#>= \case ("213", "", "INV" : _) -> True; _ -> False
|
||||
it "with port and keyHash" $ ("214", "", "NEW localhost:5000#" <> testKeyHashStr) >#>= \case ("214", "", "INV" : _) -> True; _ -> False
|
||||
describe "invalid" do
|
||||
-- TODO: add tests with defined connection alias
|
||||
it "no parameters" $ ("221", "", "NEW") >#> ("221", "", "ERR CMD SYNTAX")
|
||||
it "many parameters" $ ("222", "", "NEW localhost:5000 hi") >#> ("222", "", "ERR CMD SYNTAX")
|
||||
it "invalid server keyHash" $ ("223", "", "NEW localhost:5000#1") >#> ("223", "", "ERR CMD SYNTAX")
|
||||
|
||||
describe "JOIN" do
|
||||
describe "valid" do
|
||||
-- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided)
|
||||
-- TODO: add tests with defined connection alias
|
||||
it "using same server as in invitation" $
|
||||
("311", "", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "", "ERR SMP AUTH")
|
||||
describe "invalid" do
|
||||
-- TODO: JOIN is not merged yet - to be added
|
||||
it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX")
|
||||
@@ -0,0 +1,389 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module AgentTests.SQLiteTests (storeTests) where
|
||||
|
||||
import Control.Monad.Except (ExceptT, runExceptT)
|
||||
import qualified Crypto.PubKey.RSA as R
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time
|
||||
import Data.Word (Word32)
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import SMPClient (testKeyHash)
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import System.Random (Random (randomIO))
|
||||
import Test.Hspec
|
||||
import UnliftIO.Directory (removeFile)
|
||||
|
||||
testDB :: String
|
||||
testDB = "tests/tmp/smp-agent.test.db"
|
||||
|
||||
withStore :: SpecWith SQLiteStore -> Spec
|
||||
withStore = before createStore . after removeStore
|
||||
where
|
||||
createStore :: IO SQLiteStore
|
||||
createStore = do
|
||||
-- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous
|
||||
-- IO operations on multiple similarly named files; error seems to be environment specific
|
||||
r <- randomIO :: IO Word32
|
||||
createSQLiteStore $ testDB <> show r
|
||||
|
||||
removeStore :: SQLiteStore -> IO ()
|
||||
removeStore store = do
|
||||
DB.close $ dbConn store
|
||||
removeFile $ dbFilePath store
|
||||
|
||||
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
|
||||
storeTests :: Spec
|
||||
storeTests = withStore do
|
||||
describe "store setup" do
|
||||
testCompiledThreadsafe
|
||||
testForeignKeysEnabled
|
||||
describe "store methods" do
|
||||
describe "Queue and Connection management" do
|
||||
describe "createRcvConn" do
|
||||
testCreateRcvConn
|
||||
testCreateRcvConnDuplicate
|
||||
describe "createSndConn" do
|
||||
testCreateSndConn
|
||||
testCreateSndConnDuplicate
|
||||
describe "getAllConnAliases" testGetAllConnAliases
|
||||
describe "getRcvQueue" testGetRcvQueue
|
||||
describe "deleteConn" do
|
||||
testDeleteRcvConn
|
||||
testDeleteSndConn
|
||||
testDeleteDuplexConn
|
||||
describe "upgradeRcvConnToDuplex" do
|
||||
testUpgradeRcvConnToDuplex
|
||||
describe "upgradeSndConnToDuplex" do
|
||||
testUpgradeSndConnToDuplex
|
||||
describe "set Queue status" do
|
||||
describe "setRcvQueueStatus" do
|
||||
testSetRcvQueueStatus
|
||||
testSetRcvQueueStatusNoQueue
|
||||
describe "setSndQueueStatus" do
|
||||
testSetSndQueueStatus
|
||||
testSetSndQueueStatusNoQueue
|
||||
testSetQueueStatusDuplex
|
||||
describe "Msg management" do
|
||||
describe "create Msg" do
|
||||
testCreateRcvMsg
|
||||
testCreateSndMsg
|
||||
testCreateRcvAndSndMsgs
|
||||
|
||||
testCompiledThreadsafe :: SpecWith SQLiteStore
|
||||
testCompiledThreadsafe = do
|
||||
it "compiled sqlite library should be threadsafe" $ \store -> do
|
||||
compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]]
|
||||
compileOptions `shouldNotContain` [["THREADSAFE=0"]]
|
||||
|
||||
testForeignKeysEnabled :: SpecWith SQLiteStore
|
||||
testForeignKeysEnabled = do
|
||||
it "foreign keys should be enabled" $ \store -> do
|
||||
let inconsistentQuery =
|
||||
[sql|
|
||||
INSERT INTO connections
|
||||
(conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id)
|
||||
VALUES
|
||||
("conn1", "smp.simplex.im", "5223", "1234", "smp.simplex.im", "5223", "2345");
|
||||
|]
|
||||
DB.execute_ (dbConn store) inconsistentQuery
|
||||
`shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint)
|
||||
|
||||
rcvQueue1 :: RcvQueue
|
||||
rcvQueue1 =
|
||||
RcvQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
rcvId = "1234",
|
||||
connAlias = "conn1",
|
||||
rcvPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
sndId = Just "2345",
|
||||
sndKey = Nothing,
|
||||
decryptKey = C.safePrivateKey (1, 2, 3),
|
||||
verifyKey = Nothing,
|
||||
status = New
|
||||
}
|
||||
|
||||
sndQueue1 :: SndQueue
|
||||
sndQueue1 =
|
||||
SndQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
sndId = "3456",
|
||||
connAlias = "conn1",
|
||||
sndPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
encryptKey = C.PublicKey $ R.PublicKey 1 2 3,
|
||||
signKey = C.safePrivateKey (1, 2, 3),
|
||||
status = New
|
||||
}
|
||||
|
||||
testCreateRcvConn :: SpecWith SQLiteStore
|
||||
testCreateRcvConn = do
|
||||
it "should create RcvConnection and add SndQueue" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
|
||||
upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
|
||||
testCreateRcvConnDuplicate :: SpecWith SQLiteStore
|
||||
testCreateRcvConnDuplicate = do
|
||||
it "should throw error on attempt to create duplicate RcvConnection" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
createRcvConn store rcvQueue1
|
||||
`throwsError` SEConnDuplicate
|
||||
|
||||
testCreateSndConn :: SpecWith SQLiteStore
|
||||
testCreateSndConn = do
|
||||
it "should create SndConnection and add RcvQueue" $ \store -> do
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
|
||||
upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
|
||||
testCreateSndConnDuplicate :: SpecWith SQLiteStore
|
||||
testCreateSndConnDuplicate = do
|
||||
it "should throw error on attempt to create duplicate SndConnection" $ \store -> do
|
||||
_ <- runExceptT $ createSndConn store sndQueue1
|
||||
createSndConn store sndQueue1
|
||||
`throwsError` SEConnDuplicate
|
||||
|
||||
testGetAllConnAliases :: SpecWith SQLiteStore
|
||||
testGetAllConnAliases = do
|
||||
it "should get all conn aliases" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
_ <- runExceptT $ createSndConn store sndQueue1 {connAlias = "conn2"}
|
||||
getAllConnAliases store
|
||||
`returnsResult` ["conn1" :: ConnAlias, "conn2" :: ConnAlias]
|
||||
|
||||
testGetRcvQueue :: SpecWith SQLiteStore
|
||||
testGetRcvQueue = do
|
||||
it "should get RcvQueue" $ \store -> do
|
||||
let smpServer = SMPServer "smp.simplex.im" (Just "5223") testKeyHash
|
||||
let recipientId = "1234"
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
getRcvQueue store smpServer recipientId
|
||||
`returnsResult` rcvQueue1
|
||||
|
||||
testDeleteRcvConn :: SpecWith SQLiteStore
|
||||
testDeleteRcvConn = do
|
||||
it "should create RcvConnection and delete it" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testDeleteSndConn :: SpecWith SQLiteStore
|
||||
testDeleteSndConn = do
|
||||
it "should create SndConnection and delete it" $ \store -> do
|
||||
_ <- runExceptT $ createSndConn store sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testDeleteDuplexConn :: SpecWith SQLiteStore
|
||||
testDeleteDuplexConn = do
|
||||
it "should create DuplexConnection and delete it" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testUpgradeRcvConnToDuplex :: SpecWith SQLiteStore
|
||||
testUpgradeRcvConnToDuplex = do
|
||||
it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" $ \store -> do
|
||||
_ <- runExceptT $ createSndConn store sndQueue1
|
||||
let anotherSndQueue =
|
||||
SndQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
sndId = "2345",
|
||||
connAlias = "conn1",
|
||||
sndPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
encryptKey = C.PublicKey $ R.PublicKey 1 2 3,
|
||||
signKey = C.safePrivateKey (1, 2, 3),
|
||||
status = New
|
||||
}
|
||||
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
|
||||
`throwsError` SEBadConnType CSnd
|
||||
_ <- runExceptT $ upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
|
||||
`throwsError` SEBadConnType CDuplex
|
||||
|
||||
testUpgradeSndConnToDuplex :: SpecWith SQLiteStore
|
||||
testUpgradeSndConnToDuplex = do
|
||||
it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
let anotherRcvQueue =
|
||||
RcvQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
rcvId = "3456",
|
||||
connAlias = "conn1",
|
||||
rcvPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
sndId = Just "4567",
|
||||
sndKey = Nothing,
|
||||
decryptKey = C.safePrivateKey (1, 2, 3),
|
||||
verifyKey = Nothing,
|
||||
status = New
|
||||
}
|
||||
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
|
||||
`throwsError` SEBadConnType CRcv
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
|
||||
`throwsError` SEBadConnType CDuplex
|
||||
|
||||
testSetRcvQueueStatus :: SpecWith SQLiteStore
|
||||
testSetRcvQueueStatus = do
|
||||
it "should update status of RcvQueue" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
|
||||
setRcvQueueStatus store rcvQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1 {status = Confirmed})
|
||||
|
||||
testSetSndQueueStatus :: SpecWith SQLiteStore
|
||||
testSetSndQueueStatus = do
|
||||
it "should update status of SndQueue" $ \store -> do
|
||||
_ <- runExceptT $ createSndConn store sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1 {status = Confirmed})
|
||||
|
||||
testSetQueueStatusDuplex :: SpecWith SQLiteStore
|
||||
testSetQueueStatusDuplex = do
|
||||
it "should update statuses of RcvQueue and SndQueue in DuplexConnection" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
setRcvQueueStatus store rcvQueue1 Secured
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1)
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed})
|
||||
|
||||
testSetRcvQueueStatusNoQueue :: SpecWith SQLiteStore
|
||||
testSetRcvQueueStatusNoQueue = do
|
||||
xit "should throw error on attempt to update status of non-existent RcvQueue" $ \store -> do
|
||||
setRcvQueueStatus store rcvQueue1 Confirmed
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testSetSndQueueStatusNoQueue :: SpecWith SQLiteStore
|
||||
testSetSndQueueStatusNoQueue = do
|
||||
xit "should throw error on attempt to update status of non-existent SndQueue" $ \store -> do
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
hw :: ByteString
|
||||
hw = encodeUtf8 "Hello world!"
|
||||
|
||||
ts :: UTCTime
|
||||
ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
|
||||
|
||||
mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> BrokerId -> MsgHash -> RcvMsgData
|
||||
mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
|
||||
RcvMsgData
|
||||
{ internalId,
|
||||
internalRcvId,
|
||||
internalTs = ts,
|
||||
senderMeta = (externalSndId, ts),
|
||||
brokerMeta = (brokerId, ts),
|
||||
msgBody = hw,
|
||||
internalHash,
|
||||
externalPrevSndHash = "hash_from_sender",
|
||||
msgIntegrity = MsgOk
|
||||
}
|
||||
|
||||
testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> RcvQueue -> RcvMsgData -> Expectation
|
||||
testCreateRcvMsg' store expectedPrevSndId expectedPrevHash rcvQueue rcvMsgData@RcvMsgData {..} = do
|
||||
updateRcvIds store rcvQueue
|
||||
`returnsResult` (internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)
|
||||
createRcvMsg store rcvQueue rcvMsgData
|
||||
`returnsResult` ()
|
||||
|
||||
testCreateRcvMsg :: SpecWith SQLiteStore
|
||||
testCreateRcvMsg = do
|
||||
it "should reserve internal ids and create a RcvMsg" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
-- TODO getMsg to check message
|
||||
testCreateRcvMsg' store 0 "" rcvQueue1 $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy"
|
||||
testCreateRcvMsg' store 1 "hash_dummy" rcvQueue1 $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy"
|
||||
|
||||
mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData
|
||||
mkSndMsgData internalId internalSndId internalHash =
|
||||
SndMsgData
|
||||
{ internalId,
|
||||
internalSndId,
|
||||
internalTs = ts,
|
||||
msgBody = hw,
|
||||
internalHash
|
||||
}
|
||||
|
||||
testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> SndQueue -> SndMsgData -> Expectation
|
||||
testCreateSndMsg' store expectedPrevHash sndQueue sndMsgData@SndMsgData {..} = do
|
||||
updateSndIds store sndQueue
|
||||
`returnsResult` (internalId, internalSndId, expectedPrevHash)
|
||||
createSndMsg store sndQueue sndMsgData
|
||||
`returnsResult` ()
|
||||
|
||||
testCreateSndMsg :: SpecWith SQLiteStore
|
||||
testCreateSndMsg = do
|
||||
it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \store -> do
|
||||
_ <- runExceptT $ createSndConn store sndQueue1
|
||||
-- TODO getMsg to check message
|
||||
testCreateSndMsg' store "" sndQueue1 $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy"
|
||||
testCreateSndMsg' store "hash_dummy" sndQueue1 $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy"
|
||||
|
||||
testCreateRcvAndSndMsgs :: SpecWith SQLiteStore
|
||||
testCreateRcvAndSndMsgs = do
|
||||
it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \store -> do
|
||||
_ <- runExceptT $ createRcvConn store rcvQueue1
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
testCreateRcvMsg' store 0 "" rcvQueue1 $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1"
|
||||
testCreateRcvMsg' store 1 "rcv_hash_1" rcvQueue1 $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2"
|
||||
testCreateSndMsg' store "" sndQueue1 $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1"
|
||||
testCreateRcvMsg' store 2 "rcv_hash_2" rcvQueue1 $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
|
||||
testCreateSndMsg' store "snd_hash_1" sndQueue1 $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
|
||||
testCreateSndMsg' store "snd_hash_2" sndQueue1 $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
|
||||
@@ -0,0 +1,128 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module MarkdownTests where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Simplex.Markdown
|
||||
import System.Console.ANSI.Types
|
||||
import Test.Hspec
|
||||
|
||||
markdownTests :: Spec
|
||||
markdownTests = do
|
||||
textFormat
|
||||
secretText
|
||||
textColor
|
||||
|
||||
textFormat :: Spec
|
||||
textFormat = describe "text format (bold)" do
|
||||
it "correct markdown" do
|
||||
parseMarkdown "this is *bold formatted* text"
|
||||
`shouldBe` "this is " <> Markdown Bold "bold formatted" <> " " <> "text"
|
||||
parseMarkdown "*bold formatted* text"
|
||||
`shouldBe` Markdown Bold "bold formatted" <> " " <> "text"
|
||||
parseMarkdown "this is *bold*"
|
||||
`shouldBe` "this is " <> Markdown Bold "bold"
|
||||
parseMarkdown " *bold* text"
|
||||
`shouldBe` " " <> Markdown Bold "bold" <> " " <> "text"
|
||||
parseMarkdown " *bold* text"
|
||||
`shouldBe` " " <> Markdown Bold "bold" <> " " <> "text"
|
||||
parseMarkdown "this is *bold* "
|
||||
`shouldBe` "this is " <> Markdown Bold "bold" <> " "
|
||||
parseMarkdown "this is *bold* "
|
||||
`shouldBe` "this is " <> Markdown Bold "bold" <> " "
|
||||
it "ignored as markdown" do
|
||||
parseMarkdown "this is * unformatted * text"
|
||||
`shouldBe` "this is " <> "* unformatted *" <> " " <> "text"
|
||||
parseMarkdown "this is *unformatted * text"
|
||||
`shouldBe` "this is " <> "*unformatted *" <> " " <> "text"
|
||||
parseMarkdown "this is * unformatted* text"
|
||||
`shouldBe` "this is " <> "* unformatted*" <> " " <> "text"
|
||||
parseMarkdown "this is **unformatted** text"
|
||||
`shouldBe` "this is " <> "**" <> "unformatted** text"
|
||||
parseMarkdown "this is*unformatted* text"
|
||||
`shouldBe` "this is*unformatted* text"
|
||||
parseMarkdown "this is *unformatted text"
|
||||
`shouldBe` "this is " <> "*unformatted text"
|
||||
it "ignored internal markdown" do
|
||||
parseMarkdown "this is *long _bold_ (not italic)* text"
|
||||
`shouldBe` "this is " <> Markdown Bold "long _bold_ (not italic)" <> " " <> "text"
|
||||
parseMarkdown "snippet: `this is *bold text*`"
|
||||
`shouldBe` "snippet: " <> Markdown Snippet "this is *bold text*"
|
||||
|
||||
secretText :: Spec
|
||||
secretText = describe "secret text" do
|
||||
it "correct markdown" do
|
||||
parseMarkdown "this is #black_secret# text"
|
||||
`shouldBe` "this is " <> Markdown Secret "black_secret" <> " " <> "text"
|
||||
parseMarkdown "##black_secret### text"
|
||||
`shouldBe` Markdown Secret "#black_secret##" <> " " <> "text"
|
||||
parseMarkdown "this is #black secret# text"
|
||||
`shouldBe` "this is " <> Markdown Secret "black secret" <> " " <> "text"
|
||||
parseMarkdown "##black secret### text"
|
||||
`shouldBe` Markdown Secret "#black secret##" <> " " <> "text"
|
||||
parseMarkdown "this is #secret#"
|
||||
`shouldBe` "this is " <> Markdown Secret "secret"
|
||||
parseMarkdown " #secret# text"
|
||||
`shouldBe` " " <> Markdown Secret "secret" <> " " <> "text"
|
||||
parseMarkdown " #secret# text"
|
||||
`shouldBe` " " <> Markdown Secret "secret" <> " " <> "text"
|
||||
parseMarkdown "this is #secret# "
|
||||
`shouldBe` "this is " <> Markdown Secret "secret" <> " "
|
||||
parseMarkdown "this is #secret# "
|
||||
`shouldBe` "this is " <> Markdown Secret "secret" <> " "
|
||||
it "ignored as markdown" do
|
||||
parseMarkdown "this is # unformatted # text"
|
||||
`shouldBe` "this is " <> "# unformatted #" <> " " <> "text"
|
||||
parseMarkdown "this is #unformatted # text"
|
||||
`shouldBe` "this is " <> "#unformatted #" <> " " <> "text"
|
||||
parseMarkdown "this is # unformatted# text"
|
||||
`shouldBe` "this is " <> "# unformatted#" <> " " <> "text"
|
||||
parseMarkdown "this is ## unformatted ## text"
|
||||
`shouldBe` "this is " <> "## unformatted ##" <> " " <> "text"
|
||||
parseMarkdown "this is#unformatted# text"
|
||||
`shouldBe` "this is#unformatted# text"
|
||||
parseMarkdown "this is #unformatted text"
|
||||
`shouldBe` "this is " <> "#unformatted text"
|
||||
it "ignored internal markdown" do
|
||||
parseMarkdown "snippet: `this is #secret_text#`"
|
||||
`shouldBe` "snippet: " <> Markdown Snippet "this is #secret_text#"
|
||||
|
||||
red :: Text -> Markdown
|
||||
red = Markdown (Colored Red)
|
||||
|
||||
textColor :: Spec
|
||||
textColor = describe "text color (red)" do
|
||||
it "correct markdown" do
|
||||
parseMarkdown "this is !1 red color! text"
|
||||
`shouldBe` "this is " <> red "red color" <> " " <> "text"
|
||||
parseMarkdown "!1 red! text"
|
||||
`shouldBe` red "red" <> " " <> "text"
|
||||
parseMarkdown "this is !1 red!"
|
||||
`shouldBe` "this is " <> red "red"
|
||||
parseMarkdown " !1 red! text"
|
||||
`shouldBe` " " <> red "red" <> " " <> "text"
|
||||
parseMarkdown " !1 red! text"
|
||||
`shouldBe` " " <> red "red" <> " " <> "text"
|
||||
parseMarkdown "this is !1 red! "
|
||||
`shouldBe` "this is " <> red "red" <> " "
|
||||
parseMarkdown "this is !1 red! "
|
||||
`shouldBe` "this is " <> red "red" <> " "
|
||||
it "ignored as markdown" do
|
||||
parseMarkdown "this is !1 unformatted ! text"
|
||||
`shouldBe` "this is " <> "!1 unformatted !" <> " " <> "text"
|
||||
parseMarkdown "this is !1 unformatted ! text"
|
||||
`shouldBe` "this is " <> "!1 unformatted !" <> " " <> "text"
|
||||
parseMarkdown "this is !1 unformatted! text"
|
||||
`shouldBe` "this is " <> "!1 unformatted!" <> " " <> "text"
|
||||
-- parseMarkdown "this is !!1 unformatted!! text"
|
||||
-- `shouldBe` "this is " <> "!!1" <> "unformatted!! text"
|
||||
parseMarkdown "this is!1 unformatted! text"
|
||||
`shouldBe` "this is!1 unformatted! text"
|
||||
parseMarkdown "this is !1 unformatted text"
|
||||
`shouldBe` "this is " <> "!1 unformatted text"
|
||||
it "ignored internal markdown" do
|
||||
parseMarkdown "this is !1 long *red* (not bold)! text"
|
||||
`shouldBe` "this is " <> red "long *red* (not bold)" <> " " <> "text"
|
||||
parseMarkdown "snippet: `this is !1 red text!`"
|
||||
`shouldBe` "snippet: " <> Markdown Snippet "this is !1 red text!"
|
||||
@@ -0,0 +1,18 @@
|
||||
module ProtocolErrorTests where
|
||||
|
||||
import Simplex.Messaging.Agent.Transmission (AgentErrorType, agentErrorTypeP, serializeAgentError)
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ErrorType, errorTypeP, serializeErrorType)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.QuickCheck (modifyMaxSuccess)
|
||||
import Test.QuickCheck
|
||||
|
||||
protocolErrorTests :: Spec
|
||||
protocolErrorTests = modifyMaxSuccess (const 1000) $ do
|
||||
describe "errors parsing / serializing" $ do
|
||||
it "should parse SMP protocol errors" . property $ \err ->
|
||||
parseAll errorTypeP (serializeErrorType err)
|
||||
== Right (err :: ErrorType)
|
||||
it "should parse SMP agent errors" . property $ \err ->
|
||||
parseAll agentErrorTypeP (serializeAgentError err)
|
||||
== Right (err :: AgentErrorType)
|
||||
@@ -0,0 +1,144 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module SMPAgentClient where
|
||||
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import SMPClient (serverBracket, testPort, withSmpServer, withSmpServerThreadOn)
|
||||
import Simplex.Messaging.Agent (runSMPAgentBlocking)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Client (SMPClientConfig (..), smpDefaultConfig)
|
||||
import Simplex.Messaging.Transport
|
||||
import Test.Hspec
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.IO
|
||||
|
||||
agentTestHost :: HostName
|
||||
agentTestHost = "localhost"
|
||||
|
||||
agentTestPort :: ServiceName
|
||||
agentTestPort = "5001"
|
||||
|
||||
agentTestPort2 :: ServiceName
|
||||
agentTestPort2 = "5011"
|
||||
|
||||
agentTestPort3 :: ServiceName
|
||||
agentTestPort3 = "5021"
|
||||
|
||||
testDB :: String
|
||||
testDB = "tests/tmp/smp-agent.test.protocol.db"
|
||||
|
||||
testDB2 :: String
|
||||
testDB2 = "tests/tmp/smp-agent2.test.protocol.db"
|
||||
|
||||
testDB3 :: String
|
||||
testDB3 = "tests/tmp/smp-agent3.test.protocol.db"
|
||||
|
||||
smpAgentTest :: ARawTransmission -> IO ARawTransmission
|
||||
smpAgentTest cmd = runSmpAgentTest $ \h -> tPutRaw h cmd >> tGetRaw h
|
||||
|
||||
runSmpAgentTest :: (MonadUnliftIO m, MonadRandom m) => (Handle -> m a) -> m a
|
||||
runSmpAgentTest test = withSmpServer . withSmpAgent $ testSMPAgentClient test
|
||||
|
||||
runSmpAgentServerTest :: (MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> Handle -> m a) -> m a
|
||||
runSmpAgentServerTest test =
|
||||
withSmpServerThreadOn testPort $
|
||||
\server -> withSmpAgentThreadOn (agentTestPort, testDB) $
|
||||
\agent -> testSMPAgentClient $ test (server, agent)
|
||||
|
||||
smpAgentServerTest :: ((ThreadId, ThreadId) -> Handle -> IO ()) -> Expectation
|
||||
smpAgentServerTest test' = runSmpAgentServerTest test' `shouldReturn` ()
|
||||
|
||||
runSmpAgentTestN :: forall m a. (MonadUnliftIO m, MonadRandom m) => [(ServiceName, String)] -> ([Handle] -> m a) -> m a
|
||||
runSmpAgentTestN agents test = withSmpServer $ run agents []
|
||||
where
|
||||
run :: [(ServiceName, String)] -> [Handle] -> m a
|
||||
run [] hs = test hs
|
||||
run (a@(p, _) : as) hs = withSmpAgentOn a $ testSMPAgentClientOn p $ \h -> run as (h : hs)
|
||||
|
||||
runSmpAgentTestN_1 :: forall m a. (MonadUnliftIO m, MonadRandom m) => Int -> ([Handle] -> m a) -> m a
|
||||
runSmpAgentTestN_1 nClients test = withSmpServer . withSmpAgent $ run nClients []
|
||||
where
|
||||
run :: Int -> [Handle] -> m a
|
||||
run 0 hs = test hs
|
||||
run n hs = testSMPAgentClient $ \h -> run (n - 1) (h : hs)
|
||||
|
||||
smpAgentTestN :: [(ServiceName, String)] -> ([Handle] -> IO ()) -> Expectation
|
||||
smpAgentTestN agents test' = runSmpAgentTestN agents test' `shouldReturn` ()
|
||||
|
||||
smpAgentTestN_1 :: Int -> ([Handle] -> IO ()) -> Expectation
|
||||
smpAgentTestN_1 n test' = runSmpAgentTestN_1 n test' `shouldReturn` ()
|
||||
|
||||
smpAgentTest2 :: (Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest2 test' =
|
||||
smpAgentTestN [(agentTestPort, testDB), (agentTestPort2, testDB2)] _test
|
||||
where
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpAgentTest2_1 :: (Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest2_1 test' = smpAgentTestN_1 2 _test
|
||||
where
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpAgentTest3 :: (Handle -> Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest3 test' =
|
||||
smpAgentTestN
|
||||
[(agentTestPort, testDB), (agentTestPort2, testDB2), (agentTestPort3, testDB3)]
|
||||
_test
|
||||
where
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
smpAgentTest3_1 :: (Handle -> Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest3_1 test' = smpAgentTestN_1 3 _test
|
||||
where
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
{ tcpPort = agentTestPort,
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 1,
|
||||
dbFile = testDB,
|
||||
smpCfg =
|
||||
smpDefaultConfig
|
||||
{ qSize = 1,
|
||||
defaultPort = testPort,
|
||||
tcpTimeout = 500_000
|
||||
}
|
||||
}
|
||||
|
||||
withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => (ServiceName, String) -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn (port', db') =
|
||||
serverBracket
|
||||
(\started -> runSMPAgentBlocking started cfg {tcpPort = port', dbFile = db'})
|
||||
(removeFile db')
|
||||
|
||||
withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => (ServiceName, String) -> m a -> m a
|
||||
withSmpAgentOn (port', db') = withSmpAgentThreadOn (port', db') . const
|
||||
|
||||
withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => m a -> m a
|
||||
withSmpAgent = withSmpAgentOn (agentTestPort, testDB)
|
||||
|
||||
testSMPAgentClientOn :: MonadUnliftIO m => ServiceName -> (Handle -> m a) -> m a
|
||||
testSMPAgentClientOn port' client = do
|
||||
runTCPClient agentTestHost port' $ \h -> do
|
||||
line <- liftIO $ getLn h
|
||||
if line == "Welcome to SMP v0.2.0 agent"
|
||||
then client h
|
||||
else error "not connected"
|
||||
|
||||
testSMPAgentClient :: MonadUnliftIO m => (Handle -> m a) -> m a
|
||||
testSMPAgentClient = testSMPAgentClientOn agentTestPort
|
||||
+118
-33
@@ -1,20 +1,30 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module SMPClient where
|
||||
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Env.STM
|
||||
import Data.ByteString.Base64 (encode)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Network.Socket
|
||||
import Server
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server (runSMPServerBlocking)
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Simplex.Messaging.Server.StoreLog (openReadStoreLog)
|
||||
import Simplex.Messaging.Transport
|
||||
import Test.Hspec
|
||||
import Transmission
|
||||
import Transport
|
||||
import UnliftIO.Concurrent
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.IO
|
||||
import UnliftIO.STM (TMVar, atomically, newEmptyTMVarIO, takeTMVar)
|
||||
import UnliftIO.Timeout (timeout)
|
||||
|
||||
testHost :: HostName
|
||||
testHost = "localhost"
|
||||
@@ -22,57 +32,132 @@ testHost = "localhost"
|
||||
testPort :: ServiceName
|
||||
testPort = "5000"
|
||||
|
||||
testSMPClient :: MonadUnliftIO m => (Handle -> m a) -> m a
|
||||
testSMPClient client = do
|
||||
threadDelay 5000 -- TODO hack: thread delay for SMP server to start
|
||||
runTCPClient testHost testPort $ \h -> do
|
||||
line <- getLn h
|
||||
if line == "Welcome to SMP"
|
||||
then client h
|
||||
else error "not connected"
|
||||
testKeyHashStr :: B.ByteString
|
||||
testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
|
||||
|
||||
cfg :: Config
|
||||
testKeyHash :: Maybe C.KeyHash
|
||||
testKeyHash = Just "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
|
||||
|
||||
testStoreLogFile :: FilePath
|
||||
testStoreLogFile = "tests/tmp/smp-server-store.log"
|
||||
|
||||
testSMPClient :: MonadUnliftIO m => (THandle -> m a) -> m a
|
||||
testSMPClient client =
|
||||
runTCPClient testHost testPort $ \h ->
|
||||
liftIO (runExceptT $ clientHandshake h testKeyHash) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
|
||||
cfg :: ServerConfig
|
||||
cfg =
|
||||
Config
|
||||
ServerConfig
|
||||
{ tcpPort = testPort,
|
||||
tbqSize = 1,
|
||||
queueIdBytes = 12,
|
||||
msgIdBytes = 6
|
||||
msgIdBytes = 6,
|
||||
storeLog = Nothing,
|
||||
serverPrivateKey =
|
||||
-- full RSA private key (only for tests)
|
||||
"MIIFIwIBAAKCAQEArZyrri/NAwt5buvYjwu+B/MQeJUszDBpRgVqNddlI9kNwDXu\
|
||||
\kaJ8chEhrtaUgXeSWGooWwqjXEUQE6RVbCC6QVo9VEBSP4xFwVVd9Fj7OsgfcXXh\
|
||||
\AqWxfctDcBZQ5jTUiJpdBc+Vz2ZkumVNl0W+j9kWm9nfkMLQj8c0cVSDxz4OKpZb\
|
||||
\qFuj0uzHkis7e7wsrKSKWLPg3M5ZXPZM1m9qn7SfJzDRDfJifamxWI7uz9XK2+Dp\
|
||||
\NkUQlGQgFJEv1cKN88JAwIqZ1s+TAQMQiB+4QZ2aNfSqGEzRJN7FMCKRK7pM0A9A\
|
||||
\PCnijyuImvKFxTdk8Bx1q+XNJzsY6fBrLWJZ+QKBgQCySG4tzlcEm+tOVWRcwrWh\
|
||||
\6zsczGZp9mbf9c8itRx6dlldSYuDG1qnddL70wuAZF2AgS1JZgvcRZECoZRoWP5q\
|
||||
\Kq2wvpTIYjFPpC39lxgUoA/DXKVKZZdan+gwaVPAPT54my1CS32VrOiAY4gVJ3LJ\
|
||||
\Mn1/FqZXUFQA326pau3loQKCAQEAoljmJMp88EZoy3HlHUbOjl5UEhzzVsU1TnQi\
|
||||
\QmPm+aWRe2qelhjW4aTvSVE5mAUJsN6UWTeMf4uvM69Z9I5pfw2pEm8x4+GxRibY\
|
||||
\iiwF2QNaLxxmzEHm1zQQPTgb39o8mgklhzFPill0JsnL3f6IkVwjFJofWSmpqEGs\
|
||||
\dFSMRSXUTVXh1p/o7QZrhpwO/475iWKVS7o48N/0Xp513re3aXw+DRNuVnFEaBIe\
|
||||
\TLvWM9Czn16ndAu1HYiTBuMvtRbAWnGZxU8ewzF4wlWK5tdIL5PTJDd1VhZJAKtB\
|
||||
\npDvJpwxzKmjAhcTmjx0ckMIWtdVaOVm/2gWCXDty2FEdg7koQKBgQDOUUguJ/i7\
|
||||
\q0jldWYRnVkotKnpInPdcEaodrehfOqYEHnvro9xlS6OeAS4Vz5AdH45zQ/4J3bV\
|
||||
\2cH66tNr18ebM9nL//t5G69i89R9W7szyUxCI3LmAIdi3oSEbmz5GQBaw4l6h9Wi\
|
||||
\n4FmFQaAXZrjQfO2qJcAHvWRsMp2pmqAGwKBgQDXaza0DRsKWywWznsHcmHa0cx8\
|
||||
\I4jxqGaQmLO7wBJRP1NSFrywy1QfYrVX9CTLBK4V3F0PCgZ01Qv94751CzN43TgF\
|
||||
\ebd/O9r5NjNTnOXzdWqETbCffLGd6kLgCMwPQWpM9ySVjXHWCGZsRAnF2F6M1O32\
|
||||
\43StIifvwJQFqSM3ewKBgCaW6y7sRY90Ua7283RErezd9EyT22BWlDlACrPu3FNC\
|
||||
\LtBf1j43uxBWBQrMLsHe2GtTV0xt9m0MfwZsm2gSsXcm4Xi4DJgfN+Z7rIlyy9UY\
|
||||
\PCDSdZiU1qSr+NrffDrXlfiAM1cUmCdUX7eKjp/ltkUHNaOGfSn5Pdr3MkAiD/Hf\
|
||||
\AoGBAKIdKCuOwuYlwjS9J+IRGuSSM4o+OxQdwGmcJDTCpyWb5dEk68e7xKIna3zf\
|
||||
\jc+H+QdMXv1nkRK9bZgYheXczsXaNZUSTwpxaEldzVD3hNvsXSgJRy9fqHwA4PBq\
|
||||
\vqiBHoO3RNbqg+2rmTMfDuXreME3S955ZiPZm4Z+T8Hj52mPAoGAQm5QH/gLFtY5\
|
||||
\+znqU/0G8V6BKISCQMxbbmTQVcTgGySrP2gVd+e4MWvUttaZykhWqs8rpr7mgpIY\
|
||||
\hul7Swx0SHFN3WpXu8uj+B6MLpRcCbDHO65qU4kQLs+IaXXsuuTjMvJ5LwjkZVrQ\
|
||||
\TmKzSAw7iVWwEUZR/PeiEKazqrpp9VU="
|
||||
}
|
||||
|
||||
withSmpServer :: (MonadUnliftIO m, MonadRandom m) => m a -> m a
|
||||
withSmpServer = E.bracket (forkIO $ runSMPServer cfg) (liftIO . killThread) . const
|
||||
withSmpServerStoreLogOn :: (MonadUnliftIO m, MonadRandom m) => ServiceName -> (ThreadId -> m a) -> m a
|
||||
withSmpServerStoreLogOn port client = do
|
||||
s <- liftIO $ openReadStoreLog testStoreLogFile
|
||||
serverBracket
|
||||
(\started -> runSMPServerBlocking started cfg {tcpPort = port, storeLog = Just s})
|
||||
(pure ())
|
||||
client
|
||||
|
||||
runSmpTest :: (MonadUnliftIO m, MonadRandom m) => (Handle -> m a) -> m a
|
||||
withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ServiceName -> (ThreadId -> m a) -> m a
|
||||
withSmpServerThreadOn port =
|
||||
serverBracket
|
||||
(\started -> runSMPServerBlocking started cfg {tcpPort = port})
|
||||
(pure ())
|
||||
|
||||
serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a
|
||||
serverBracket process afterProcess f = do
|
||||
started <- newEmptyTMVarIO
|
||||
E.bracket
|
||||
(forkIOWithUnmask ($ process started))
|
||||
(\t -> killThread t >> afterProcess >> waitFor started "stop")
|
||||
(\t -> waitFor started "start" >> f t)
|
||||
where
|
||||
waitFor started s =
|
||||
5_000_000 `timeout` atomically (takeTMVar started) >>= \case
|
||||
Nothing -> error $ "server did not " <> s
|
||||
_ -> pure ()
|
||||
|
||||
withSmpServerOn :: (MonadUnliftIO m, MonadRandom m) => ServiceName -> m a -> m a
|
||||
withSmpServerOn port = withSmpServerThreadOn port . const
|
||||
|
||||
withSmpServer :: (MonadUnliftIO m, MonadRandom m) => m a -> m a
|
||||
withSmpServer = withSmpServerOn testPort
|
||||
|
||||
runSmpTest :: (MonadUnliftIO m, MonadRandom m) => (THandle -> m a) -> m a
|
||||
runSmpTest test = withSmpServer $ testSMPClient test
|
||||
|
||||
runSmpTestN :: forall m a. (MonadUnliftIO m, MonadRandom m) => Int -> ([Handle] -> m a) -> m a
|
||||
runSmpTestN nClients test = withSmpServer $ run [] nClients
|
||||
runSmpTestN :: forall m a. (MonadUnliftIO m, MonadRandom m) => Int -> ([THandle] -> m a) -> m a
|
||||
runSmpTestN nClients test = withSmpServer $ run nClients []
|
||||
where
|
||||
run :: [Handle] -> Int -> m a
|
||||
run hs 0 = test hs
|
||||
run hs n = testSMPClient $ \h -> run (h : hs) (n - 1)
|
||||
run :: Int -> [THandle] -> m a
|
||||
run 0 hs = test hs
|
||||
run n hs = testSMPClient $ \h -> run (n - 1) (h : hs)
|
||||
|
||||
smpServerTest :: [RawTransmission] -> IO [RawTransmission]
|
||||
smpServerTest commands = runSmpTest \h -> mapM (sendReceive h) commands
|
||||
where
|
||||
sendReceive :: Handle -> RawTransmission -> IO RawTransmission
|
||||
sendReceive h t = tPutRaw h t >> either (error "bad transmission") id <$> tGetRaw h
|
||||
smpServerTest :: RawTransmission -> IO RawTransmission
|
||||
smpServerTest cmd = runSmpTest $ \h -> tPutRaw h cmd >> tGetRaw h
|
||||
|
||||
smpTest :: (Handle -> IO ()) -> Expectation
|
||||
smpTest :: (THandle -> IO ()) -> Expectation
|
||||
smpTest test' = runSmpTest test' `shouldReturn` ()
|
||||
|
||||
smpTestN :: Int -> ([Handle] -> IO ()) -> Expectation
|
||||
smpTestN :: Int -> ([THandle] -> IO ()) -> Expectation
|
||||
smpTestN n test' = runSmpTestN n test' `shouldReturn` ()
|
||||
|
||||
smpTest2 :: (Handle -> Handle -> IO ()) -> Expectation
|
||||
smpTest2 :: (THandle -> THandle -> IO ()) -> Expectation
|
||||
smpTest2 test' = smpTestN 2 _test
|
||||
where
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpTest3 :: (Handle -> Handle -> Handle -> IO ()) -> Expectation
|
||||
smpTest3 :: (THandle -> THandle -> THandle -> IO ()) -> Expectation
|
||||
smpTest3 test' = smpTestN 3 _test
|
||||
where
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
tPutRaw :: THandle -> RawTransmission -> IO ()
|
||||
tPutRaw h (sig, corrId, queueId, command) = do
|
||||
let t = B.intercalate " " [corrId, queueId, command]
|
||||
void $ tPut h (C.Signature sig, t)
|
||||
|
||||
tGetRaw :: THandle -> IO RawTransmission
|
||||
tGetRaw h = do
|
||||
("", (CorrId corrId, qId, Right cmd)) <- tGet fromServer h
|
||||
pure ("", corrId, encode qId, serializeCommand cmd)
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module ServerTests where
|
||||
|
||||
import Control.Concurrent (ThreadId, killThread)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (SomeException, try)
|
||||
import Control.Monad.Except (forM_, runExceptT)
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import SMPClient
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Transport
|
||||
import System.Directory (removeFile)
|
||||
import System.TimeIt (timeItT)
|
||||
import System.Timeout
|
||||
import Test.HUnit
|
||||
import Test.Hspec
|
||||
|
||||
rsaKeySize :: Int
|
||||
rsaKeySize = 2048 `div` 8
|
||||
|
||||
serverTests :: Spec
|
||||
serverTests = do
|
||||
describe "SMP syntax" syntaxTests
|
||||
describe "SMP queues" do
|
||||
describe "NEW and KEY commands, SEND messages" testCreateSecure
|
||||
describe "NEW, OFF and DEL commands, SEND messages" testCreateDelete
|
||||
describe "SMP messages" do
|
||||
describe "duplex communication over 2 SMP connections" testDuplex
|
||||
describe "switch subscription to another SMP queue" testSwitchSub
|
||||
describe "Store log" testWithStoreLog
|
||||
describe "Timing of AUTH error" testTiming
|
||||
|
||||
pattern Resp :: CorrId -> QueueId -> Command 'Broker -> SignedTransmissionOrError
|
||||
pattern Resp corrId queueId command <- ("", (corrId, queueId, Right (Cmd SBroker command)))
|
||||
|
||||
sendRecv :: THandle -> (ByteString, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
|
||||
sendRecv h (sgn, corrId, qId, cmd) = tPutRaw h (sgn, corrId, encode qId, cmd) >> tGet fromServer h
|
||||
|
||||
signSendRecv :: THandle -> C.SafePrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
|
||||
signSendRecv h pk (corrId, qId, cmd) = do
|
||||
let t = B.intercalate " " [corrId, encode qId, cmd]
|
||||
Right sig <- runExceptT $ C.sign pk t
|
||||
_ <- tPut h (sig, t)
|
||||
tGet fromServer h
|
||||
|
||||
cmdSEND :: ByteString -> ByteString
|
||||
cmdSEND msg = serializeCommand (Cmd SSender . SEND $ msg)
|
||||
|
||||
(>#>) :: RawTransmission -> RawTransmission -> Expectation
|
||||
command >#> response = smpServerTest command `shouldReturn` response
|
||||
|
||||
(#==) :: (HasCallStack, Eq a, Show a) => (a, a) -> String -> Assertion
|
||||
(actual, expected) #== message = assertEqual message expected actual
|
||||
|
||||
testCreateSecure :: Spec
|
||||
testCreateSecure =
|
||||
it "should create (NEW) and secure (KEY) queue" $
|
||||
smpTest \h -> do
|
||||
(rPub, rKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" rId1 (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
|
||||
(rId1, "") #== "creates queue"
|
||||
|
||||
Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, "SEND 5 hello ")
|
||||
(ok1, OK) #== "accepts unsigned SEND"
|
||||
(sId1, sId) #== "same queue ID in response 1"
|
||||
|
||||
Resp "" _ (MSG _ _ msg1) <- tGet fromServer h
|
||||
(msg1, "hello") #== "delivers message"
|
||||
|
||||
Resp "cdab" _ ok4 <- signSendRecv h rKey ("cdab", rId, "ACK")
|
||||
(ok4, OK) #== "replies OK when message acknowledged if no more messages"
|
||||
|
||||
Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, "ACK")
|
||||
(err6, ERR NO_MSG) #== "replies ERR when message acknowledged without messages"
|
||||
|
||||
(sPub, sKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" sId2 err1 <- signSendRecv h sKey ("abcd", sId, "SEND 5 hello ")
|
||||
(err1, ERR AUTH) #== "rejects signed SEND"
|
||||
(sId2, sId) #== "same queue ID in response 2"
|
||||
|
||||
let keyCmd = "KEY " <> C.serializePubKey sPub
|
||||
Resp "bcda" _ err2 <- sendRecv h (sampleSig, "bcda", rId, keyCmd)
|
||||
(err2, ERR AUTH) #== "rejects KEY with wrong signature"
|
||||
|
||||
Resp "cdab" _ err3 <- signSendRecv h rKey ("cdab", sId, keyCmd)
|
||||
(err3, ERR AUTH) #== "rejects KEY with sender's ID"
|
||||
|
||||
Resp "dabc" rId2 ok2 <- signSendRecv h rKey ("dabc", rId, keyCmd)
|
||||
(ok2, OK) #== "secures queue"
|
||||
(rId2, rId) #== "same queue ID in response 3"
|
||||
|
||||
Resp "abcd" _ err4 <- signSendRecv h rKey ("abcd", rId, keyCmd)
|
||||
(err4, ERR AUTH) #== "rejects KEY if already secured"
|
||||
|
||||
Resp "bcda" _ ok3 <- signSendRecv h sKey ("bcda", sId, "SEND 11 hello again ")
|
||||
(ok3, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp "" _ (MSG _ _ msg) <- tGet fromServer h
|
||||
(msg, "hello again") #== "delivers message 2"
|
||||
|
||||
Resp "cdab" _ ok5 <- signSendRecv h rKey ("cdab", rId, "ACK")
|
||||
(ok5, OK) #== "replies OK when message acknowledged 2"
|
||||
|
||||
Resp "dabc" _ err5 <- sendRecv h ("", "dabc", sId, "SEND 5 hello ")
|
||||
(err5, ERR AUTH) #== "rejects unsigned SEND"
|
||||
|
||||
testCreateDelete :: Spec
|
||||
testCreateDelete =
|
||||
it "should create (NEW), suspend (OFF) and delete (DEL) queue" $
|
||||
smpTest2 \rh sh -> do
|
||||
(rPub, rKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" rId1 (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
|
||||
(rId1, "") #== "creates queue"
|
||||
|
||||
(sPub, sKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, "KEY " <> C.serializePubKey sPub)
|
||||
(ok1, OK) #== "secures queue"
|
||||
|
||||
Resp "cdab" _ ok2 <- signSendRecv sh sKey ("cdab", sId, "SEND 5 hello ")
|
||||
(ok2, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp "dabc" _ ok7 <- signSendRecv sh sKey ("dabc", sId, "SEND 7 hello 2 ")
|
||||
(ok7, OK) #== "accepts signed SEND 2 - this message is not delivered because the first is not ACKed"
|
||||
|
||||
Resp "" _ (MSG _ _ msg1) <- tGet fromServer rh
|
||||
(msg1, "hello") #== "delivers message"
|
||||
|
||||
Resp "abcd" _ err1 <- sendRecv rh (sampleSig, "abcd", rId, "OFF")
|
||||
(err1, ERR AUTH) #== "rejects OFF with wrong signature"
|
||||
|
||||
Resp "bcda" _ err2 <- signSendRecv rh rKey ("bcda", sId, "OFF")
|
||||
(err2, ERR AUTH) #== "rejects OFF with sender's ID"
|
||||
|
||||
Resp "cdab" rId2 ok3 <- signSendRecv rh rKey ("cdab", rId, "OFF")
|
||||
(ok3, OK) #== "suspends queue"
|
||||
(rId2, rId) #== "same queue ID in response 2"
|
||||
|
||||
Resp "dabc" _ err3 <- signSendRecv sh sKey ("dabc", sId, "SEND 5 hello ")
|
||||
(err3, ERR AUTH) #== "rejects signed SEND"
|
||||
|
||||
Resp "abcd" _ err4 <- sendRecv sh ("", "abcd", sId, "SEND 5 hello ")
|
||||
(err4, ERR AUTH) #== "reject unsigned SEND too"
|
||||
|
||||
Resp "bcda" _ ok4 <- signSendRecv rh rKey ("bcda", rId, "OFF")
|
||||
(ok4, OK) #== "accepts OFF when suspended"
|
||||
|
||||
Resp "cdab" _ (MSG _ _ msg) <- signSendRecv rh rKey ("cdab", rId, "SUB")
|
||||
(msg, "hello") #== "accepts SUB when suspended and delivers the message again (because was not ACKed)"
|
||||
|
||||
Resp "dabc" _ err5 <- sendRecv rh (sampleSig, "dabc", rId, "DEL")
|
||||
(err5, ERR AUTH) #== "rejects DEL with wrong signature"
|
||||
|
||||
Resp "abcd" _ err6 <- signSendRecv rh rKey ("abcd", sId, "DEL")
|
||||
(err6, ERR AUTH) #== "rejects DEL with sender's ID"
|
||||
|
||||
Resp "bcda" rId3 ok6 <- signSendRecv rh rKey ("bcda", rId, "DEL")
|
||||
(ok6, OK) #== "deletes queue"
|
||||
(rId3, rId) #== "same queue ID in response 3"
|
||||
|
||||
Resp "cdab" _ err7 <- signSendRecv sh sKey ("cdab", sId, "SEND 5 hello ")
|
||||
(err7, ERR AUTH) #== "rejects signed SEND when deleted"
|
||||
|
||||
Resp "dabc" _ err8 <- sendRecv sh ("", "dabc", sId, "SEND 5 hello ")
|
||||
(err8, ERR AUTH) #== "rejects unsigned SEND too when deleted"
|
||||
|
||||
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, "ACK")
|
||||
(err11, ERR AUTH) #== "rejects ACK when conn deleted - the second message is deleted"
|
||||
|
||||
Resp "bcda" _ err9 <- signSendRecv rh rKey ("bcda", rId, "OFF")
|
||||
(err9, ERR AUTH) #== "rejects OFF when deleted"
|
||||
|
||||
Resp "cdab" _ err10 <- signSendRecv rh rKey ("cdab", rId, "SUB")
|
||||
(err10, ERR AUTH) #== "rejects SUB when deleted"
|
||||
|
||||
testDuplex :: Spec
|
||||
testDuplex =
|
||||
it "should create 2 simplex connections and exchange messages" $
|
||||
smpTest2 \alice bob -> do
|
||||
(arPub, arKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" _ (IDS aRcv aSnd) <- signSendRecv alice arKey ("abcd", "", "NEW " <> C.serializePubKey arPub)
|
||||
-- aSnd ID is passed to Bob out-of-band
|
||||
|
||||
(bsPub, bsKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, cmdSEND $ "key " <> C.serializePubKey bsPub)
|
||||
-- "key ..." is ad-hoc, different from SMP protocol
|
||||
|
||||
Resp "" _ (MSG _ _ msg1) <- tGet fromServer alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, "ACK")
|
||||
["key", bobKey] <- return $ B.words msg1
|
||||
(bobKey, C.serializePubKey bsPub) #== "key received from Bob"
|
||||
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, "KEY " <> bobKey)
|
||||
|
||||
(brPub, brKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" _ (IDS bRcv bSnd) <- signSendRecv bob brKey ("abcd", "", "NEW " <> C.serializePubKey brPub)
|
||||
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, cmdSEND $ "reply_id " <> encode bSnd)
|
||||
-- "reply_id ..." is ad-hoc, it is not a part of SMP protocol
|
||||
|
||||
Resp "" _ (MSG _ _ msg2) <- tGet fromServer alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, "ACK")
|
||||
["reply_id", bId] <- return $ B.words msg2
|
||||
(bId, encode bSnd) #== "reply queue ID received from Bob"
|
||||
|
||||
(asPub, asKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, cmdSEND $ "key " <> C.serializePubKey asPub)
|
||||
-- "key ..." is ad-hoc, different from SMP protocol
|
||||
|
||||
Resp "" _ (MSG _ _ msg3) <- tGet fromServer bob
|
||||
Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, "ACK")
|
||||
["key", aliceKey] <- return $ B.words msg3
|
||||
(aliceKey, C.serializePubKey asPub) #== "key received from Alice"
|
||||
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, "KEY " <> aliceKey)
|
||||
|
||||
Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, "SEND 8 hi alice ")
|
||||
|
||||
Resp "" _ (MSG _ _ msg4) <- tGet fromServer alice
|
||||
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, "ACK")
|
||||
(msg4, "hi alice") #== "message received from Bob"
|
||||
|
||||
Resp "abcd" _ OK <- signSendRecv alice asKey ("abcd", bSnd, cmdSEND "how are you bob")
|
||||
|
||||
Resp "" _ (MSG _ _ msg5) <- tGet fromServer bob
|
||||
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, "ACK")
|
||||
(msg5, "how are you bob") #== "message received from alice"
|
||||
|
||||
testSwitchSub :: Spec
|
||||
testSwitchSub =
|
||||
it "should create simplex connections and switch subscription to another TCP connection" $
|
||||
smpTest3 \rh1 rh2 sh -> do
|
||||
(rPub, rKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" _ (IDS rId sId) <- signSendRecv rh1 rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
|
||||
Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, "SEND 5 test1 ")
|
||||
(ok1, OK) #== "sent test message 1"
|
||||
Resp "cdab" _ ok2 <- sendRecv sh ("", "cdab", sId, cmdSEND "test2, no ACK")
|
||||
(ok2, OK) #== "sent test message 2"
|
||||
|
||||
Resp "" _ (MSG _ _ msg1) <- tGet fromServer rh1
|
||||
(msg1, "test1") #== "test message 1 delivered to the 1st TCP connection"
|
||||
Resp "abcd" _ (MSG _ _ msg2) <- signSendRecv rh1 rKey ("abcd", rId, "ACK")
|
||||
(msg2, "test2, no ACK") #== "test message 2 delivered, no ACK"
|
||||
|
||||
Resp "bcda" _ (MSG _ _ msg2') <- signSendRecv rh2 rKey ("bcda", rId, "SUB")
|
||||
(msg2', "test2, no ACK") #== "same simplex queue via another TCP connection, tes2 delivered again (no ACK in 1st queue)"
|
||||
Resp "cdab" _ OK <- signSendRecv rh2 rKey ("cdab", rId, "ACK")
|
||||
|
||||
Resp "" _ end <- tGet fromServer rh1
|
||||
(end, END) #== "unsubscribed the 1st TCP connection"
|
||||
|
||||
Resp "dabc" _ OK <- sendRecv sh ("", "dabc", sId, "SEND 5 test3 ")
|
||||
|
||||
Resp "" _ (MSG _ _ msg3) <- tGet fromServer rh2
|
||||
(msg3, "test3") #== "delivered to the 2nd TCP connection"
|
||||
|
||||
Resp "abcd" _ err <- signSendRecv rh1 rKey ("abcd", rId, "ACK")
|
||||
(err, ERR NO_MSG) #== "rejects ACK from the 1st TCP connection"
|
||||
|
||||
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, "ACK")
|
||||
(ok3, OK) #== "accepts ACK from the 2nd TCP connection"
|
||||
|
||||
1000 `timeout` tGet fromServer rh1 >>= \case
|
||||
Nothing -> return ()
|
||||
Just _ -> error "nothing else is delivered to the 1st TCP connection"
|
||||
|
||||
testWithStoreLog :: Spec
|
||||
testWithStoreLog =
|
||||
it "should store simplex queues to log and restore them after server restart" $ do
|
||||
(sPub1, sKey1) <- C.generateKeyPair rsaKeySize
|
||||
(sPub2, sKey2) <- C.generateKeyPair rsaKeySize
|
||||
senderId1 <- newTVarIO ""
|
||||
senderId2 <- newTVarIO ""
|
||||
|
||||
withSmpServerStoreLogOn testPort . runTest $ \h -> do
|
||||
(sId1, _, _) <- createAndSecureQueue h sPub1
|
||||
atomically $ writeTVar senderId1 sId1
|
||||
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
|
||||
Resp "" _ (MSG _ _ "hello") <- tGet fromServer h
|
||||
|
||||
(sId2, rId2, rKey2) <- createAndSecureQueue h sPub2
|
||||
atomically $ writeTVar senderId2 sId2
|
||||
Resp "cdab" _ OK <- signSendRecv h sKey2 ("cdab", sId2, "SEND 9 hello too ")
|
||||
Resp "" _ (MSG _ _ "hello too") <- tGet fromServer h
|
||||
|
||||
Resp "dabc" _ OK <- signSendRecv h rKey2 ("dabc", rId2, "DEL")
|
||||
pure ()
|
||||
|
||||
logSize `shouldReturn` 5
|
||||
|
||||
withSmpServerThreadOn testPort . runTest $ \h -> do
|
||||
sId1 <- readTVarIO senderId1
|
||||
-- fails if store log is disabled
|
||||
Resp "bcda" _ (ERR AUTH) <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
|
||||
pure ()
|
||||
|
||||
withSmpServerStoreLogOn testPort . runTest $ \h -> do
|
||||
-- this queue is restored
|
||||
sId1 <- readTVarIO senderId1
|
||||
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
|
||||
-- this queue is removed - not restored
|
||||
sId2 <- readTVarIO senderId2
|
||||
Resp "cdab" _ (ERR AUTH) <- signSendRecv h sKey2 ("cdab", sId2, "SEND 9 hello too ")
|
||||
pure ()
|
||||
|
||||
logSize `shouldReturn` 1
|
||||
removeFile testStoreLogFile
|
||||
where
|
||||
createAndSecureQueue :: THandle -> SenderPublicKey -> IO (SenderId, RecipientId, C.SafePrivateKey)
|
||||
createAndSecureQueue h sPub = do
|
||||
(rPub, rKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" "" (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
|
||||
let keyCmd = "KEY " <> C.serializePubKey sPub
|
||||
Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, keyCmd)
|
||||
(rId', rId) #== "same queue ID"
|
||||
pure (sId, rId, rKey)
|
||||
|
||||
runTest :: (THandle -> IO ()) -> ThreadId -> Expectation
|
||||
runTest test' server = do
|
||||
testSMPClient test' `shouldReturn` ()
|
||||
killThread server
|
||||
|
||||
logSize :: IO Int
|
||||
logSize =
|
||||
try (length . B.lines <$> B.readFile testStoreLogFile) >>= \case
|
||||
Right l -> pure l
|
||||
Left (_ :: SomeException) -> logSize
|
||||
|
||||
testTiming :: Spec
|
||||
testTiming =
|
||||
it "should have similar time for auth error, whether queue exists or not, for all key sizes" $
|
||||
smpTest2 \rh sh ->
|
||||
mapM_
|
||||
(testSameTiming rh sh)
|
||||
[ (128, 128, 100),
|
||||
(128, 256, 25),
|
||||
-- (128, 512, 15),
|
||||
(256, 128, 100),
|
||||
(256, 256, 25)
|
||||
-- (256, 512, 15),
|
||||
-- (512, 128, 100),
|
||||
-- (512, 256, 25),
|
||||
-- (512, 512, 15)
|
||||
]
|
||||
where
|
||||
timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const
|
||||
similarTime t1 t2 = abs (t1 - t2) / t1 < 0.2 `shouldBe` True
|
||||
testSameTiming :: THandle -> THandle -> (Int, Int, Int) -> Expectation
|
||||
testSameTiming rh sh (senderKeySize, badKeySize, n) = do
|
||||
(rPub, rKey) <- C.generateKeyPair rsaKeySize
|
||||
Resp "abcd" "" (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
|
||||
|
||||
(sPub, sKey) <- C.generateKeyPair senderKeySize
|
||||
let keyCmd = "KEY " <> C.serializePubKey sPub
|
||||
Resp "dabc" _ OK <- signSendRecv rh rKey ("dabc", rId, keyCmd)
|
||||
|
||||
(_, badKey) <- C.generateKeyPair badKeySize
|
||||
Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, "SEND 5 hello ")
|
||||
timeWrongKey <- timeRepeat n $ do
|
||||
Resp "cdab" _ (ERR AUTH) <- signSendRecv sh badKey ("cdab", sId, "SEND 5 hello ")
|
||||
return ()
|
||||
timeNoQueue <- timeRepeat n $ do
|
||||
Resp "dabc" _ (ERR AUTH) <- signSendRecv sh badKey ("dabc", "1234", "SEND 5 hello ")
|
||||
return ()
|
||||
Resp "" _ (MSG _ _ "hello") <- tGet fromServer rh
|
||||
similarTime timeNoQueue timeWrongKey
|
||||
|
||||
samplePubKey :: ByteString
|
||||
samplePubKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR"
|
||||
|
||||
sampleSig :: ByteString
|
||||
sampleSig = "\128\207*\159eq\220i!\"\157\161\130\184\226\246\232_\\\170`\180\160\230sI\154\197\211\252\SUB\246\206ELL\t9K\ESC\196?\128\215%\222\148\NAK;9\155f\164\217e\242\156\CAN9\253\r\170\174'w\211\228?\205)\215\150\255\247z\DC115\DC1{\bn\145\rKD,K\230\202d8\233\167|7y\t_S\EM\248\EOT\216\172\167d\181\224)\137\ACKo\197j#c\217\243\228.\167\228\205\144\vr\134"
|
||||
|
||||
syntaxTests :: Spec
|
||||
syntaxTests = do
|
||||
it "unknown command" $ ("", "abcd", "1234", "HELLO") >#> ("", "abcd", "1234", "ERR CMD SYNTAX")
|
||||
describe "NEW" do
|
||||
it "no parameters" $ (sampleSig, "bcda", "", "NEW") >#> ("", "bcda", "", "ERR CMD SYNTAX")
|
||||
it "many parameters" $ (sampleSig, "cdab", "", "NEW 1 " <> samplePubKey) >#> ("", "cdab", "", "ERR CMD SYNTAX")
|
||||
it "no signature" $ ("", "dabc", "", "NEW " <> samplePubKey) >#> ("", "dabc", "", "ERR CMD NO_AUTH")
|
||||
it "queue ID" $ (sampleSig, "abcd", "12345678", "NEW " <> samplePubKey) >#> ("", "abcd", "12345678", "ERR CMD HAS_AUTH")
|
||||
describe "KEY" do
|
||||
it "valid syntax" $ (sampleSig, "bcda", "12345678", "KEY " <> samplePubKey) >#> ("", "bcda", "12345678", "ERR AUTH")
|
||||
it "no parameters" $ (sampleSig, "cdab", "12345678", "KEY") >#> ("", "cdab", "12345678", "ERR CMD SYNTAX")
|
||||
it "many parameters" $ (sampleSig, "dabc", "12345678", "KEY 1 " <> samplePubKey) >#> ("", "dabc", "12345678", "ERR CMD SYNTAX")
|
||||
it "no signature" $ ("", "abcd", "12345678", "KEY " <> samplePubKey) >#> ("", "abcd", "12345678", "ERR CMD NO_AUTH")
|
||||
it "no queue ID" $ (sampleSig, "bcda", "", "KEY " <> samplePubKey) >#> ("", "bcda", "", "ERR CMD NO_AUTH")
|
||||
noParamsSyntaxTest "SUB"
|
||||
noParamsSyntaxTest "ACK"
|
||||
noParamsSyntaxTest "OFF"
|
||||
noParamsSyntaxTest "DEL"
|
||||
describe "SEND" do
|
||||
it "valid syntax 1" $ (sampleSig, "cdab", "12345678", "SEND 5 hello ") >#> ("", "cdab", "12345678", "ERR AUTH")
|
||||
it "valid syntax 2" $ (sampleSig, "dabc", "12345678", "SEND 11 hello there ") >#> ("", "dabc", "12345678", "ERR AUTH")
|
||||
it "no parameters" $ (sampleSig, "abcd", "12345678", "SEND") >#> ("", "abcd", "12345678", "ERR CMD SYNTAX")
|
||||
it "no queue ID" $ (sampleSig, "bcda", "", "SEND 5 hello ") >#> ("", "bcda", "", "ERR CMD NO_QUEUE")
|
||||
it "bad message body 1" $ (sampleSig, "cdab", "12345678", "SEND 11 hello ") >#> ("", "cdab", "12345678", "ERR CMD SYNTAX")
|
||||
it "bad message body 2" $ (sampleSig, "dabc", "12345678", "SEND hello ") >#> ("", "dabc", "12345678", "ERR CMD SYNTAX")
|
||||
it "bigger body" $ (sampleSig, "abcd", "12345678", "SEND 4 hello ") >#> ("", "abcd", "12345678", "ERR CMD SYNTAX")
|
||||
describe "PING" do
|
||||
it "valid syntax" $ ("", "abcd", "", "PING") >#> ("", "abcd", "", "PONG")
|
||||
describe "broker response not allowed" do
|
||||
it "OK" $ (sampleSig, "bcda", "12345678", "OK") >#> ("", "bcda", "12345678", "ERR CMD PROHIBITED")
|
||||
where
|
||||
noParamsSyntaxTest :: ByteString -> Spec
|
||||
noParamsSyntaxTest cmd = describe (B.unpack cmd) do
|
||||
it "valid syntax" $ (sampleSig, "abcd", "12345678", cmd) >#> ("", "abcd", "12345678", "ERR AUTH")
|
||||
it "wrong terminator" $ (sampleSig, "bcda", "12345678", cmd <> "=") >#> ("", "bcda", "12345678", "ERR CMD SYNTAX")
|
||||
it "no signature" $ ("", "cdab", "12345678", cmd) >#> ("", "cdab", "12345678", "ERR CMD NO_AUTH")
|
||||
it "no queue ID" $ (sampleSig, "dabc", "", cmd) >#> ("", "dabc", "", "ERR CMD NO_AUTH")
|
||||
+13
-270
@@ -1,273 +1,16 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either
|
||||
import SMPClient
|
||||
import System.IO (Handle)
|
||||
import System.Timeout
|
||||
import Test.HUnit
|
||||
import AgentTests
|
||||
import MarkdownTests
|
||||
import ProtocolErrorTests
|
||||
import ServerTests
|
||||
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
|
||||
import Test.Hspec
|
||||
import Transmission
|
||||
import Transport
|
||||
|
||||
main :: IO ()
|
||||
main = hspec do
|
||||
describe "SMP syntax" syntaxTests
|
||||
describe "SMP queues" do
|
||||
describe "NEW and KEY commands, SEND messages" testCreateSecure
|
||||
describe "NEW, OFF and DEL commands, SEND messages" testCreateDelete
|
||||
describe "SMP messages" do
|
||||
describe "duplex communication over 2 SMP connections" testDuplex
|
||||
describe "switch subscription to another SMP queue" testSwitchSub
|
||||
|
||||
pattern Resp :: QueueId -> Command 'Broker -> TransmissionOrError
|
||||
pattern Resp queueId command = ("", (queueId, Right (Cmd SBroker command)))
|
||||
|
||||
sendRecv :: Handle -> RawTransmission -> IO TransmissionOrError
|
||||
sendRecv h (sgn, qId, cmd) = tPutRaw h (fromRight "" $ decode sgn, qId, cmd) >> tGet fromServer h
|
||||
|
||||
(>#>) :: [RawTransmission] -> [RawTransmission] -> Expectation
|
||||
commands >#> responses = smpServerTest commands `shouldReturn` responses
|
||||
|
||||
(#==) :: (HasCallStack, Eq a, Show a) => (a, a) -> String -> Assertion
|
||||
(actual, expected) #== message = assertEqual message expected actual
|
||||
|
||||
testCreateSecure :: SpecWith ()
|
||||
testCreateSecure =
|
||||
it "should create (NEW) and secure (KEY) queue" $
|
||||
smpTest \h -> do
|
||||
Resp rId1 (IDS rId sId) <- sendRecv h ("", "", "NEW 1234")
|
||||
(rId1, "") #== "creates queue"
|
||||
|
||||
Resp sId1 ok1 <- sendRecv h ("", sId, "SEND :hello")
|
||||
(ok1, OK) #== "accepts unsigned SEND"
|
||||
(sId1, sId) #== "same queue ID in response 1"
|
||||
|
||||
Resp _ (MSG _ _ msg1) <- tGet fromServer h
|
||||
(msg1, "hello") #== "delivers message"
|
||||
|
||||
Resp _ ok4 <- sendRecv h ("1234", rId, "ACK")
|
||||
(ok4, OK) #== "replies OK when message acknowledged if no more messages"
|
||||
|
||||
Resp _ err6 <- sendRecv h ("1234", rId, "ACK")
|
||||
(err6, ERR PROHIBITED) #== "replies ERR when message acknowledged without messages"
|
||||
|
||||
Resp sId2 err1 <- sendRecv h ("4567", sId, "SEND :hello")
|
||||
(err1, ERR AUTH) #== "rejects signed SEND"
|
||||
(sId2, sId) #== "same queue ID in response 2"
|
||||
|
||||
Resp _ err2 <- sendRecv h ("12345678", rId, "KEY 4567")
|
||||
(err2, ERR AUTH) #== "rejects KEY with wrong signature (password atm)"
|
||||
|
||||
Resp _ err3 <- sendRecv h ("1234", sId, "KEY 4567")
|
||||
(err3, ERR AUTH) #== "rejects KEY with sender's ID"
|
||||
|
||||
Resp rId2 ok2 <- sendRecv h ("1234", rId, "KEY 4567")
|
||||
(ok2, OK) #== "secures queue"
|
||||
(rId2, rId) #== "same queue ID in response 3"
|
||||
|
||||
Resp _ err4 <- sendRecv h ("1234", rId, "KEY 4567")
|
||||
(err4, ERR AUTH) #== "rejects KEY if already secured"
|
||||
|
||||
Resp _ ok3 <- sendRecv h ("4567", sId, "SEND 11\nhello again")
|
||||
(ok3, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp _ (MSG _ _ msg) <- tGet fromServer h
|
||||
(msg, "hello again") #== "delivers message 2"
|
||||
|
||||
Resp _ ok5 <- sendRecv h ("1234", rId, "ACK")
|
||||
(ok5, OK) #== "replies OK when message acknowledged 2"
|
||||
|
||||
Resp _ err5 <- sendRecv h ("", sId, "SEND :hello")
|
||||
(err5, ERR AUTH) #== "rejects unsigned SEND"
|
||||
|
||||
testCreateDelete :: SpecWith ()
|
||||
testCreateDelete =
|
||||
it "should create (NEW), suspend (OFF) and delete (DEL) queue" $
|
||||
smpTest2 \rh sh -> do
|
||||
Resp rId1 (IDS rId sId) <- sendRecv rh ("", "", "NEW 1234")
|
||||
(rId1, "") #== "creates queue"
|
||||
|
||||
Resp _ ok1 <- sendRecv rh ("1234", rId, "KEY 4567")
|
||||
(ok1, OK) #== "secures queue"
|
||||
|
||||
Resp _ ok2 <- sendRecv sh ("4567", sId, "SEND :hello")
|
||||
(ok2, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp _ ok7 <- sendRecv sh ("4567", sId, "SEND :hello 2")
|
||||
(ok7, OK) #== "accepts signed SEND 2 - this message is not delivered because the first is not ACKed"
|
||||
|
||||
Resp _ (MSG _ _ msg1) <- tGet fromServer rh
|
||||
(msg1, "hello") #== "delivers message"
|
||||
|
||||
Resp _ err1 <- sendRecv rh ("12345678", rId, "OFF")
|
||||
(err1, ERR AUTH) #== "rejects OFF with wrong signature (password atm)"
|
||||
|
||||
Resp _ err2 <- sendRecv rh ("1234", sId, "OFF")
|
||||
(err2, ERR AUTH) #== "rejects OFF with sender's ID"
|
||||
|
||||
Resp rId2 ok3 <- sendRecv rh ("1234", rId, "OFF")
|
||||
(ok3, OK) #== "suspends queue"
|
||||
(rId2, rId) #== "same queue ID in response 2"
|
||||
|
||||
Resp _ err3 <- sendRecv sh ("4567", sId, "SEND :hello")
|
||||
(err3, ERR AUTH) #== "rejects signed SEND"
|
||||
|
||||
Resp _ err4 <- sendRecv sh ("", sId, "SEND :hello")
|
||||
(err4, ERR AUTH) #== "reject unsigned SEND too"
|
||||
|
||||
Resp _ ok4 <- sendRecv rh ("1234", rId, "OFF")
|
||||
(ok4, OK) #== "accepts OFF when suspended"
|
||||
|
||||
Resp _ (MSG _ _ msg) <- sendRecv rh ("1234", rId, "SUB")
|
||||
(msg, "hello") #== "accepts SUB when suspended and delivers the message again (because was not ACKed)"
|
||||
|
||||
Resp _ err5 <- sendRecv rh ("12345678", rId, "DEL")
|
||||
(err5, ERR AUTH) #== "rejects DEL with wrong signature (password atm)"
|
||||
|
||||
Resp _ err6 <- sendRecv rh ("1234", sId, "DEL")
|
||||
(err6, ERR AUTH) #== "rejects DEL with sender's ID"
|
||||
|
||||
Resp rId3 ok6 <- sendRecv rh ("1234", rId, "DEL")
|
||||
(ok6, OK) #== "deletes queue"
|
||||
(rId3, rId) #== "same queue ID in response 3"
|
||||
|
||||
Resp _ err7 <- sendRecv sh ("4567", sId, "SEND :hello")
|
||||
(err7, ERR AUTH) #== "rejects signed SEND when deleted"
|
||||
|
||||
Resp _ err8 <- sendRecv sh ("", sId, "SEND :hello")
|
||||
(err8, ERR AUTH) #== "rejects unsigned SEND too when deleted"
|
||||
|
||||
Resp _ err11 <- sendRecv rh ("1234", rId, "ACK")
|
||||
(err11, ERR AUTH) #== "rejects ACK when conn deleted - the second message is deleted"
|
||||
|
||||
Resp _ err9 <- sendRecv rh ("1234", rId, "OFF")
|
||||
(err9, ERR AUTH) #== "rejects OFF when deleted"
|
||||
|
||||
Resp _ err10 <- sendRecv rh ("1234", rId, "SUB")
|
||||
(err10, ERR AUTH) #== "rejects SUB when deleted"
|
||||
|
||||
testDuplex :: SpecWith ()
|
||||
testDuplex =
|
||||
it "should create 2 simplex connections and exchange messages" $
|
||||
smpTest2 \alice bob -> do
|
||||
Resp _ (IDS aRcv aSnd) <- sendRecv alice ("", "", "NEW 1234")
|
||||
-- aSnd ID is passed to Bob out-of-band
|
||||
|
||||
Resp _ OK <- sendRecv bob ("", aSnd, "SEND :key efgh")
|
||||
-- "key efgh" is ad-hoc, different from SMP protocol
|
||||
|
||||
Resp _ (MSG _ _ msg1) <- tGet fromServer alice
|
||||
Resp _ OK <- sendRecv alice ("1234", aRcv, "ACK")
|
||||
["key", key1] <- return $ B.words msg1
|
||||
(key1, "efgh") #== "key received from Bob"
|
||||
Resp _ OK <- sendRecv alice ("1234", aRcv, "KEY " <> key1)
|
||||
|
||||
Resp _ (IDS bRcv bSnd) <- sendRecv bob ("", "", "NEW abcd")
|
||||
Resp _ OK <- sendRecv bob ("efgh", aSnd, "SEND :reply_id " <> encode bSnd)
|
||||
-- "reply_id ..." is ad-hoc, it is not a part of SMP protocol
|
||||
|
||||
Resp _ (MSG _ _ msg2) <- tGet fromServer alice
|
||||
Resp _ OK <- sendRecv alice ("1234", aRcv, "ACK")
|
||||
["reply_id", bId] <- return $ B.words msg2
|
||||
(bId, encode bSnd) #== "reply queue ID received from Bob"
|
||||
Resp _ OK <- sendRecv alice ("", bSnd, "SEND :key 5678")
|
||||
-- "key 5678" is ad-hoc, different from SMP protocol
|
||||
|
||||
Resp _ (MSG _ _ msg3) <- tGet fromServer bob
|
||||
Resp _ OK <- sendRecv bob ("abcd", bRcv, "ACK")
|
||||
["key", key2] <- return $ B.words msg3
|
||||
(key2, "5678") #== "key received from Alice"
|
||||
Resp _ OK <- sendRecv bob ("abcd", bRcv, "KEY " <> key2)
|
||||
|
||||
Resp _ OK <- sendRecv bob ("efgh", aSnd, "SEND :hi alice")
|
||||
|
||||
Resp _ (MSG _ _ msg4) <- tGet fromServer alice
|
||||
Resp _ OK <- sendRecv alice ("1234", aRcv, "ACK")
|
||||
(msg4, "hi alice") #== "message received from Bob"
|
||||
|
||||
Resp _ OK <- sendRecv alice ("5678", bSnd, "SEND :how are you bob")
|
||||
|
||||
Resp _ (MSG _ _ msg5) <- tGet fromServer bob
|
||||
Resp _ OK <- sendRecv bob ("abcd", bRcv, "ACK")
|
||||
(msg5, "how are you bob") #== "message received from alice"
|
||||
|
||||
testSwitchSub :: SpecWith ()
|
||||
testSwitchSub =
|
||||
it "should create simplex connections and switch subscription to another TCP connection" $
|
||||
smpTest3 \rh1 rh2 sh -> do
|
||||
Resp _ (IDS rId sId) <- sendRecv rh1 ("", "", "NEW 1234")
|
||||
Resp _ ok1 <- sendRecv sh ("", sId, "SEND :test1")
|
||||
(ok1, OK) #== "sent test message 1"
|
||||
Resp _ ok2 <- sendRecv sh ("", sId, "SEND :test2, no ACK")
|
||||
(ok2, OK) #== "sent test message 2"
|
||||
|
||||
Resp _ (MSG _ _ msg1) <- tGet fromServer rh1
|
||||
(msg1, "test1") #== "test message 1 delivered to the 1st TCP connection"
|
||||
Resp _ (MSG _ _ msg2) <- sendRecv rh1 ("1234", rId, "ACK")
|
||||
(msg2, "test2, no ACK") #== "test message 2 delivered, no ACK"
|
||||
|
||||
Resp _ (MSG _ _ msg2') <- sendRecv rh2 ("1234", rId, "SUB")
|
||||
(msg2', "test2, no ACK") #== "same simplex queue via another TCP connection, tes2 delivered again (no ACK in 1st queue)"
|
||||
Resp _ OK <- sendRecv rh2 ("1234", rId, "ACK")
|
||||
|
||||
Resp _ end <- tGet fromServer rh1
|
||||
(end, END) #== "unsubscribed the 1st TCP connection"
|
||||
|
||||
Resp _ OK <- sendRecv sh ("", sId, "SEND :test3")
|
||||
|
||||
Resp _ (MSG _ _ msg3) <- tGet fromServer rh2
|
||||
(msg3, "test3") #== "delivered to the 2nd TCP connection"
|
||||
|
||||
Resp _ err <- sendRecv rh1 ("1234", rId, "ACK")
|
||||
(err, ERR PROHIBITED) #== "rejects ACK from the 1st TCP connection"
|
||||
|
||||
Resp _ ok3 <- sendRecv rh2 ("1234", rId, "ACK")
|
||||
(ok3, OK) #== "accepts ACK from the 2nd TCP connection"
|
||||
|
||||
timeout 1000 (tGet fromServer rh1) >>= \case
|
||||
Nothing -> return ()
|
||||
Just _ -> error "nothing else is delivered to the 1st TCP connection"
|
||||
|
||||
syntaxTests :: SpecWith ()
|
||||
syntaxTests = do
|
||||
it "unknown command" $ [("", "1234", "HELLO")] >#> [("", "1234", "ERR UNKNOWN")]
|
||||
describe "NEW" do
|
||||
it "no parameters" $ [("", "", "NEW")] >#> [("", "", "ERR SYNTAX 2")]
|
||||
it "many parameters" $ [("", "", "NEW 1 2")] >#> [("", "", "ERR SYNTAX 2")]
|
||||
it "has signature" $ [("1234", "", "NEW 1234")] >#> [("", "", "ERR SYNTAX 4")]
|
||||
it "queue ID" $ [("", "1", "NEW 1234")] >#> [("", "1", "ERR SYNTAX 4")]
|
||||
describe "KEY" do
|
||||
it "valid syntax" $ [("1234", "1", "KEY 4567")] >#> [("", "1", "ERR AUTH")]
|
||||
it "no parameters" $ [("1234", "1", "KEY")] >#> [("", "1", "ERR SYNTAX 2")]
|
||||
it "many parameters" $ [("1234", "1", "KEY 1 2")] >#> [("", "1", "ERR SYNTAX 2")]
|
||||
it "no signature" $ [("", "1", "KEY 4567")] >#> [("", "1", "ERR SYNTAX 3")]
|
||||
it "no queue ID" $ [("1234", "", "KEY 4567")] >#> [("", "", "ERR SYNTAX 3")]
|
||||
noParamsSyntaxTest "SUB"
|
||||
noParamsSyntaxTest "ACK"
|
||||
noParamsSyntaxTest "OFF"
|
||||
noParamsSyntaxTest "DEL"
|
||||
describe "SEND" do
|
||||
it "valid syntax 1" $ [("1234", "1", "SEND :hello")] >#> [("", "1", "ERR AUTH")]
|
||||
it "valid syntax 2" $ [("1234", "1", "SEND 11\nhello there\n")] >#> [("", "1", "ERR AUTH")]
|
||||
it "no parameters" $ [("1234", "1", "SEND")] >#> [("", "1", "ERR SYNTAX 2")]
|
||||
it "no queue ID" $ [("1234", "", "SEND :hello")] >#> [("", "", "ERR SYNTAX 5")]
|
||||
it "bad message body 1" $ [("1234", "1", "SEND 11 hello")] >#> [("", "1", "ERR SYNTAX 6")]
|
||||
it "bad message body 2" $ [("1234", "1", "SEND hello")] >#> [("", "1", "ERR SYNTAX 6")]
|
||||
it "bigger body" $ [("1234", "1", "SEND 4\nhello\n")] >#> [("", "1", "ERR SIZE")]
|
||||
describe "broker response not allowed" do
|
||||
it "OK" $ [("1234", "1", "OK")] >#> [("", "1", "ERR PROHIBITED")]
|
||||
where
|
||||
noParamsSyntaxTest :: ByteString -> SpecWith ()
|
||||
noParamsSyntaxTest cmd = describe (B.unpack cmd) do
|
||||
it "valid syntax" $ [("1234", "1", cmd)] >#> [("", "1", "ERR AUTH")]
|
||||
it "parameters" $ [("1234", "1", cmd <> " 1")] >#> [("", "1", "ERR SYNTAX 2")]
|
||||
it "no signature" $ [("", "1", cmd)] >#> [("", "1", "ERR SYNTAX 3")]
|
||||
it "no queue ID" $ [("1234", "", cmd)] >#> [("", "", "ERR SYNTAX 3")]
|
||||
main = do
|
||||
createDirectoryIfMissing False "tests/tmp"
|
||||
hspec $ do
|
||||
describe "SimpleX markdown" markdownTests
|
||||
describe "Protocol errors" protocolErrorTests
|
||||
describe "SMP server" serverTests
|
||||
describe "SMP client agent" agentTests
|
||||
removeDirectoryRecursive "tests/tmp"
|
||||
|
||||
Reference in New Issue
Block a user