Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd4ccce1ba | ||
|
|
584f230c4d | ||
|
|
10c62e7fa2 | ||
|
|
bd2e251cae | ||
|
|
6b5cc3d2d5 | ||
|
|
5f7fe8b0dc | ||
|
|
f15067cf68 | ||
|
|
ff2b975cd8 | ||
|
|
fe2d6607de | ||
|
|
218a7756f8 | ||
|
|
d5310c1c6f | ||
|
|
57c9ccfc08 | ||
|
|
3e226fc3f2 | ||
|
|
04c65d98da | ||
|
|
316dc7b320 | ||
|
|
606bbf5062 | ||
|
|
bfcbe4bcd8 | ||
|
|
6b5427b826 | ||
|
|
35e6593581 | ||
|
|
33de5f6fec | ||
|
|
d1d7d51fe0 | ||
|
|
0f9226ba2a | ||
|
|
862f25500d | ||
|
|
da071a0abf | ||
|
|
51491d9cd5 | ||
|
|
d973c834a5 | ||
|
|
e07bedac0e | ||
|
|
2ac903a2dd | ||
|
|
d23254f44a | ||
|
|
9a3afda290 | ||
|
|
68f24f90d1 | ||
|
|
75259d70cf | ||
|
|
96c6339306 | ||
|
|
7b5ebf7bd2 | ||
|
|
cb950ae2e4 | ||
|
|
26b0edabfc | ||
|
|
cda02a3b30 | ||
|
|
dd5137c336 | ||
|
|
e045774caa | ||
|
|
dd6a53b0d2 | ||
|
|
d9084522af | ||
|
|
3829479a21 | ||
|
|
8a4bced569 | ||
|
|
8ac4b77777 | ||
|
|
30c36b488a | ||
|
|
10fcb9771a | ||
|
|
65782d7ef6 | ||
|
|
5e380e1d47 | ||
|
|
9f945492dd | ||
|
|
8ba3e3e45a | ||
|
|
3d9ceff691 | ||
|
|
daad3315eb | ||
|
|
e4d9b481ec | ||
|
|
7af7272635 | ||
|
|
09c6adeabc | ||
|
|
d5f324cb5c | ||
|
|
bf5561c89c | ||
|
|
ab89963f45 | ||
|
|
5ebb546d58 | ||
|
|
46c3589604 | ||
|
|
dffa7a6100 | ||
|
|
bc780343df | ||
|
|
84ce001598 | ||
|
|
92bd8ef335 | ||
|
|
85535de7d8 | ||
|
|
7bbc4e4109 | ||
|
|
248aa234d1 | ||
|
|
26a0665d81 | ||
|
|
dea1845908 | ||
|
|
8ed3eb4581 | ||
|
|
5962c1bb3e | ||
|
|
5b39f51203 | ||
|
|
63532c8b45 | ||
|
|
44bec887fe | ||
|
|
8bd42035aa | ||
|
|
f7f419224f | ||
|
|
8ac47b9bbf | ||
|
|
305bc5cb0a | ||
|
|
fa5a8cb5cd | ||
|
|
5d59e4b2bd | ||
|
|
ad87442811 | ||
|
|
7aacee405e | ||
|
|
4b9ebbbab2 | ||
|
|
f09c77cd31 | ||
|
|
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 | ||
|
|
ad73298936 | ||
|
|
0fbf406800 | ||
|
|
28a3db5edc | ||
|
|
efe799f460 | ||
|
|
f8aa11f419 | ||
|
|
adc8af16b3 | ||
|
|
fc2e51053a | ||
|
|
dc223f564d | ||
|
|
ea77a4653f | ||
|
|
b80fd37a0f | ||
|
|
9d9afb1150 | ||
|
|
12e9fbf75a | ||
|
|
43c2c9d02d | ||
|
|
bea5f0525a | ||
|
|
50074eb33b | ||
|
|
dfa80d6756 | ||
|
|
1b81010e4a | ||
|
|
91470bc65d | ||
|
|
b9f4c50771 | ||
|
|
d1022dc7fc | ||
|
|
b469a90116 | ||
|
|
90d6857128 |
@@ -0,0 +1 @@
|
||||
open_collective: simplex-chat
|
||||
@@ -4,69 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
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
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/dog-food
|
||||
asset_name: dog-food-ubuntu-20_04-x86-64
|
||||
- os: ubuntu-18.04
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/dog-food
|
||||
asset_name: dog-food-ubuntu-18_04-x86-64
|
||||
# TODO re-enable tests for mac and windows when they consistently pass (remove stack_args altogether)
|
||||
- os: macos-latest
|
||||
cache_path: ~/.stack
|
||||
stack_args: ""
|
||||
artifact_rel_path: /bin/dog-food
|
||||
asset_name: dog-food-macos-x86-64
|
||||
- os: windows-latest
|
||||
cache_path: C:/sr
|
||||
stack_args: ""
|
||||
artifact_rel_path: /bin/dog-food.exe
|
||||
asset_name: dog-food-windows-x86-64
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
@@ -81,20 +26,39 @@ jobs:
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ matrix.cache_path }}
|
||||
key: ${{ matrix.os }}-${{ hashFiles('stack.yaml') }}
|
||||
path: ~/.stack
|
||||
key: ${{ hashFiles('stack.yaml') }}
|
||||
|
||||
- name: Build & test
|
||||
id: build_test
|
||||
run: |
|
||||
stack build ${{ matrix.stack_args }}
|
||||
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"
|
||||
|
||||
- name: Upload binaries to release
|
||||
stack build --test --force-dirty
|
||||
install_root=$(stack path --local-install-root)
|
||||
mv ${install_root}/bin/smp-server smp-server-ubuntu-20_04-x86-64
|
||||
|
||||
- name: Build changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.build_test.outputs.LOCAL_INSTALL_ROOT }}${{ matrix.artifact_rel_path }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
ignorePreReleases: true
|
||||
commitMode: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: |
|
||||
See full changelog [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md).
|
||||
Commits, chronological:
|
||||
${{ steps.build_changelog.outputs.changelog }}
|
||||
files: |
|
||||
LICENSE
|
||||
smp-server-ubuntu-20_04-x86-64
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
*.lock
|
||||
*.cabal
|
||||
*.db
|
||||
*.db.bak
|
||||
*.session.sql
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# 0.5.2
|
||||
|
||||
- Fix message delivery logic that blocked delivery of all server messages when server per-queue quota exceeded, making it concurrent per SMP queue, not per server.
|
||||
|
||||
# 0.5.1
|
||||
|
||||
- Fix server subscription logic bug that was leading to memory leak / resource exhaustion in some edge cases.
|
||||
|
||||
# 0.5.0
|
||||
|
||||
- No changes in SMP server implementation - it is backwards compatible with v0.4.1
|
||||
- SMP agent changes:
|
||||
- URI syntax for SMP queues and connection requests.
|
||||
- long-term connections links ("contacts") in SMP agent protocol.
|
||||
- agent command changes:
|
||||
- `REQ` notification and `ACPT` command are used only with long-term connection links.
|
||||
- `CONF` notification and `LET` commands are used for normal duplex connections.
|
||||
|
||||
# 0.4.1
|
||||
|
||||
- Include migrations in the package
|
||||
|
||||
# 0.4.0
|
||||
|
||||
- SMP server implementation and [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) changes:
|
||||
- support 3072 bit RSA key size
|
||||
- add SMP queue quotas
|
||||
- set default transport block size to 4096 bits
|
||||
- allow SMP client to change transport block size during transport connection handshake
|
||||
- SMP agent implementation and protocol changes:
|
||||
- additional connection confirmation step for initiating party
|
||||
- automatically resume subscribed duplex connections once transport connection is resumed
|
||||
- passing an arbitrary binary information between parties during the duplex connection handshake - can be used to identify parties
|
||||
- asynchronous duplex connection handshake - the parties do not have to be online at the same time
|
||||
- asynchronous message delivery - the agent does not need transport connection to accept client messages for delivery
|
||||
- additional confirmation of message reception from the client to prevent message loss in case of process termination
|
||||
- set transport block size to 8192 bits (in the future the agent protocol can allow to have different block sizes for different duplex connections)
|
||||
- added client commands and notifications (see [agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md)):
|
||||
- `REQ` - the notification about joining party establishing connection
|
||||
- `ACPT` - the command to accept connection with the joining party
|
||||
- `INFO` - the notification with the information from the initiating party
|
||||
- `DOWN`/`UP` - the notifications about losing/resuming the connection
|
||||
- `ACK` - the command to confirm that the message reception/processing is complete
|
||||
- `MID` - the response to `SEND` confirming that the message is accepted by the agent
|
||||
- `MERR` - the notification about permanent message delivery error (e.g., `ERR AUTH` indicating that the queue was removed)
|
||||
|
||||
# 0.3.2
|
||||
|
||||
- Support websockets
|
||||
- SMP server CLI commands
|
||||
|
||||
# 0.3.1
|
||||
|
||||
- Released to hackage.org
|
||||
- SMP agent protocol changes:
|
||||
- move SMP server from agent commands NEW/JOIN to agent config
|
||||
- send CON to user only when the 1st party responds HELLO
|
||||
- Fix REPLY vulnerability
|
||||
- Fix transaction busy error
|
||||
|
||||
# 0.3.0
|
||||
|
||||
- SMP encrypted transport over TCP
|
||||
- Standard X509/PKCS8 encoding for RSA keys
|
||||
- Sign and verify agent messages
|
||||
- Verify message integrity based on previous message hash and ID
|
||||
- Prevent timing attack allowing to determine if queue exists
|
||||
- Only allow correct RSA keys and signature sizes
|
||||
|
||||
# 0.2.0
|
||||
|
||||
- SMP client library
|
||||
- SMP agent with E2E encryption
|
||||
|
||||
# 0.1.0
|
||||
|
||||
- SMP protocol server implementation without encryption
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM haskell:8.8.4 AS build-stage
|
||||
# if you encounter "version `GLIBC_2.28' not found" error when running
|
||||
# chat client executable, build with the following base image instead:
|
||||
# FROM haskell:8.8.4-stretch AS build-stage
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
RUN stack install
|
||||
|
||||
FROM scratch AS export-stage
|
||||
COPY --from=build-stage /root/.local/bin/dog-food /
|
||||
@@ -1,223 +1,113 @@
|
||||
# simplex-messaging
|
||||
# SimpleXMQ
|
||||
|
||||
[](https://github.com/simplex-chat/simplex-messaging/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplex-messaging/releases)
|
||||
[](https://github.com/simplex-chat/simplexmq/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplexmq/releases)
|
||||
|
||||
## Federated chat - private, secure, decentralised
|
||||
📢 **v0.5.1 brings a hotfix to the server's subscription management logic, to apply it log in to your server via SSH and run the following command. If you have store log enabled for your server, information about already established queues will be preserved.** If you're doing a custom installation instead of Linode or DigitalOcean you may have to change the path for binary download.
|
||||
|
||||
See [simplex.chat](https://simplex.chat) website for chat demo and the explanations of the system and how SMP protocol works.
|
||||
|
||||
SMP protocol is semi-formally defined [here](https://github.com/simplex-chat/protocol).
|
||||
|
||||
Currently only these features are available:
|
||||
- simple 1-to-1 chat with multiple people in the same terminal window.
|
||||
- auto-populated recipient name - just type your messages.
|
||||
- default server is available to play with - `smp.simplex.im:5223` - and you can deploy your own (`smp-server` executable in this repo).
|
||||
- no global identity or names visible to the server(s) - for the privacy of contacts and conversations.
|
||||
- E2E encryption, with public key that has to be passed out-of-band (see below)
|
||||
- authentication of each command/message with automatically generated RSA key pairs, separate for each conversation, the keys are not used as identity (2048 bit keys are used, it can be changed in [code via rsaKeySize setting](https://github.com/simplex-chat/simplex-messaging/blob/master/apps/dog-food/Main.hs))
|
||||
|
||||
Limitations/disclaimers:
|
||||
- no support for chat groups. It is coming in the next major version (i.e., not very soon:)
|
||||
- no delivery notifications - coming soon
|
||||
- no TCP transport encryption - coming soon (messages are encrypted e2e though, only random connection IDs and server commands are visible, but not the contents of the message)
|
||||
- system and protocol security was not audited yet, so you probably should NOT use it yet for high security communications - unless you know what you are doing.
|
||||
|
||||
## How to run chat client locally
|
||||
|
||||
Install [Haskell stack](https://docs.haskellstack.org/en/stable/README/):
|
||||
|
||||
```shell
|
||||
curl -sSL https://get.haskellstack.org/ | sh
|
||||
```sh
|
||||
systemctl stop smp-server
|
||||
curl -L -o /opt/simplex/bin/smp-server https://github.com/simplex-chat/simplexmq/releases/download/v0.5.1/smp-server-ubuntu-20_04-x86-64
|
||||
systemctl start smp-server
|
||||
```
|
||||
|
||||
and build the project:
|
||||
## Message broker for unidirectional (simplex) queues
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:simplex-chat/simplex-messaging.git
|
||||
$ cd simplex-messaging
|
||||
$ stack install
|
||||
$ dog-food
|
||||
```
|
||||
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](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) for client-server communication and [SMP agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) to manage duplex connections via simplex queues on multiple SMP servers.
|
||||
|
||||
If you'd prefer to not set up Haskell locally, on Linux you may instead build the chat client executable using [docker build with custom output](https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs):
|
||||
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.
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:simplex-chat/simplex-messaging.git
|
||||
$ cd simplex-messaging
|
||||
$ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
$ dog-food
|
||||
```
|
||||
SimpleXMQ is implemented in Haskell - it benefits from robust software transactional memory (STM) and concurrency primitives that Haskell provides.
|
||||
|
||||
> **NOTE:** When running chat client executable built with the latter approach, if you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.8.4-stretch` base image instead (you'd have to change it in your local [Dockerfile](Dockerfile)).
|
||||
## SimpleXMQ roadmap
|
||||
|
||||
`dog-food` (as in "eating your own dog food" - it is an early prototype) starts chat client with default parameters. By default, app data directory is created in the home directory (`~/.simplex`, or `%APPDATA%/simplex` on Windows), and SQLite database file `smp-chat.db` is initialized in it. The default SMP server is `smp.simplex.im:5223`.
|
||||
- SMP queue redundancy and rotation in SMP agent duplex connections.
|
||||
- SMP agents synchronization to share connections and messages between multiple agents (it would allow using multiple devices for [simplex-chat](https://github.com/simplex-chat/simplex-chat)).
|
||||
- Streams - high performance message queues. See [Streams RFC](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-02-28-streams.md) for details.
|
||||
|
||||
To specify a different file path for the chat database use `-d` command line option:
|
||||
## Components
|
||||
|
||||
```shell
|
||||
$ dog-food -d my-chat.db
|
||||
```
|
||||
### SMP server
|
||||
|
||||
If you deployed your own SMP server you can set client to use it via `-s` option:
|
||||
[SMP server](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs) can be run on any Linux distribution without any dependencies. 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.
|
||||
|
||||
```shell
|
||||
$ dog-food -s smp.example.com:5223
|
||||
```
|
||||
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.
|
||||
|
||||
You can still talk to people using default or any other server, it only affects the location of the message queue when you initiate the connection (and the reply queue can be on another server, as set by the other party's client).
|
||||
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>`.
|
||||
|
||||
Run `dog-food --help` to see all available options.
|
||||
SMP server implements [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md).
|
||||
|
||||
### Using chat client
|
||||
### SMP client library
|
||||
|
||||
Once chat client is started, use `/add <name1>` to create a new connection and generate an invitation to send to your contact via any other communication channel (`<name1>` - is any name you want to use for that contact).
|
||||
[SMP client](https://github.com/simplex-chat/simplexmq/blob/master/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.
|
||||
|
||||
Invitation has format `smp::<server>::<queue_id>::<public_key_for_this_queue_only>` - this needs to be shared with another party, via any other chat. It can only be used once - even if this is intercepted, the attacker would not be able to use it to send you the messages via this queue once your contact confirms that the connection is established.
|
||||
### SMP agent
|
||||
|
||||
The party that received the invitation should use `/accept <name2> <invitation>` to accept the connection (`<name2>` is any name that the accepting party wants to use for you).
|
||||
[SMP agent library](https://github.com/simplex-chat/simplexmq/blob/master/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.
|
||||
|
||||
For example, if Alice and Bob want to chat, with Alice initiating, Alice would use [in her chat client]:
|
||||
Haskell type [ACommand](https://github.com/simplex-chat/simplexmq/blob/master/src/Simplex/Messaging/Agent/Protocol.hs) represents SMP agent protocol to communicate via STM queues.
|
||||
|
||||
```
|
||||
/add bob
|
||||
```
|
||||
See [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI for the example of integrating SMP agent into another application.
|
||||
|
||||
And then send the generated invitation to Bob out-of-band. Bob then would use [in his chat client]:
|
||||
[SMP agent executable](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs) can be used to run a standalone SMP agent process that implements plaintext [SMP agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/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.
|
||||
|
||||
```
|
||||
/accept alice <alice's invitation>
|
||||
```
|
||||
## Using SMP server and SMP agent
|
||||
|
||||
They would then use `@<name> <message>` commands to send messages. One may also press Space or just start typing a message to send a message to the contact that was the last.
|
||||
You can either run your own SMP server locally or deploy using [Linode StackScript](#deploy-smp-server-on-linode), or try local SMP agent with the deployed servers:
|
||||
|
||||
If you exit from chat client (or if internet connection is interrupted) you need to use `/chat <name>` to activate conversation with respective contact - it is not resumed automatically (it will improve soon).
|
||||
`smp2.simplex.im#z5W2QLQ1Br3Yd6CoWg7bIq1bHdwK7Y8bEiEXBs/WfAg=` (London, UK)
|
||||
`smp3.simplex.im#nxc7HnrnM8dOKgkMp008ub/9o9LXJlxlMrMpR+mfMQw=` (Fremont, CA)
|
||||
|
||||
Since SMP doesn't use global identity (all account information is managed by clients), you should configure your name to use in invitations for your contacts:
|
||||
It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.
|
||||
|
||||
```
|
||||
/name alice
|
||||
```
|
||||
[<img alt="Linode" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)
|
||||
|
||||
Now Alice's invitations would be generated with her name in it for others' convenience.
|
||||
## Deploy SMP server on Linode
|
||||
|
||||
Use `/help` in chat to see the list of available commands and their explanation.
|
||||
You can get Linode [free credits](https://www.linode.com/lp/affiliate-referral/?irclickid=02-QkdTEpxyLW0W0EOSREQreUkB2DtzGE2lGTE0&irgwc=1&utm_source=impact) to deploy SMP server.
|
||||
|
||||
### Accessing chat history
|
||||
Deployment on [Linode](https://www.linode.com/) is performed via StackScripts, which serve as recipes for Linode instances, also called Linodes. To deploy SMP server on Linode:
|
||||
|
||||
You can access your chat history by opening a connection to your SQLite database file and querying `messages` table, for example:
|
||||
- Create a Linode account or login with an already existing one.
|
||||
- Open [SMP server StackScript](https://cloud.linode.com/stackscripts/748014) and click "Deploy New Linode".
|
||||
- You can optionally configure the following parameters:
|
||||
- [SMP Server store log](#SMP-server) flag for queue persistence on server restart (recommended).
|
||||
- [Linode API token](https://www.linode.com/docs/guides/getting-started-with-the-linode-api#get-an-access-token) for attaching server info as tags to Linode (server address, public key hash, version) and adding A record to your 2nd level domain (Note: 2nd level e.g. `example.com` domain should be [created](https://cloud.linode.com/domains/create) in your account prior to deployment). The API token access scope should be read/write access to "linodes" (to update linode tags - you need them), and "domains" (to add A record for the 3rd level domain, e.g. `smp`).
|
||||
- Domain name to use instead of Linode ip address, e.g. `smp.example.com`.
|
||||
- Choose the region and plan according to your requirements (for regular use Shared CPU Nanode should be sufficient).
|
||||
- Provide ssh key to be able to connect to your Linode via ssh. This step is required if you haven't provided a Linode API token, because you will need to login to your Linode and get a public key hash either from the welcome message or from the file `/etc/opt/simplex/pub_key_hash` on your Linode after SMP server starts.
|
||||
- Deploy your Linode. After it starts wait for SMP server to start and for tags to appear (if a Linode API token was provided). It may take up to 5 minutes depending on the connection speed on the Linode. Connecting Linode IP address to provided domain name may take some additional time.
|
||||
- Get `hostname` and `hash` either from Linode tags (click on a tag and copy it's value from the browser search panel) or via ssh. Linode has a good [guide](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/) about ssh.
|
||||
- Great, your own SMP server is ready! Use `address#hash` as SMP server address in the client.
|
||||
|
||||
```sql
|
||||
select * from messages
|
||||
where conn_alias = cast('alice' as blob)
|
||||
order by internal_id desc;
|
||||
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
|
||||
|
||||
select * from messages
|
||||
where conn_alias = cast('alice' as blob)
|
||||
and body like '%cats%';
|
||||
```
|
||||
[<img alt="DigitalOcean" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/digitalocean.png" align="right" width="300">](https://marketplace.digitalocean.com/apps/simplex-server)
|
||||
|
||||
> **NOTE:** Beware that SQLite foreign key constraints are disabled by default, and must be **[enabled separately for each database connection](https://sqlite.org/foreignkeys.html#fk_enable)**. The latter can be achieved by running `PRAGMA foreign_keys = ON;` command on an open database connection. By running data altering queries without enabling foreign keys prior to that, you may risk putting your database in an inconsistent state.
|
||||
## Deploy SMP server on DigitalOcean
|
||||
|
||||
## 🚧 [further README not up to date] SMP server demo 🏗
|
||||
You can deploy SMP server using [SimpleX Server 1-click app](https://marketplace.digitalocean.com/apps/simplex-server) from DigitalOcean marketplace:
|
||||
|
||||
This is a demo implementation of SMP ([simplex messaging protocol](https://github.com/simplex-chat/protocol/blob/master/simplex-messaging.md)) server.
|
||||
- Create a DigitalOcean account or login with an already existing one.
|
||||
- Click 'Create SimpleX server Droplet' button.
|
||||
- Choose the region and plan according to your requirements (cheapest Regular plan should be sufficient).
|
||||
- Provide ssh key and confirm Droplet creation.
|
||||
- SSH to created Droplet (`ssh root@<droplet_ip_address>`) to get SMP server public key hash - either from the welcome message or from `/etc/opt/simplex/pub_key_hash`. DigitalOcean has a good guide on [how to login to Droplet via ssh](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/).
|
||||
- Great, your own SMP server is ready! Use `ip_address#hash` as SMP server address in the client.
|
||||
|
||||
It has a very limited utility (if any) for real applications, as it lacks the following protocol features:
|
||||
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
|
||||
|
||||
- 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 server design
|
||||
|
||||
Because of these limitations, it is easy to experiment with the protocol logic via telnet.
|
||||

|
||||
|
||||
You can either run it locally or try with the deployed demo server:
|
||||
## SMP agent design
|
||||
|
||||
```bash
|
||||
telnet smp.simplex.im 5223
|
||||
```
|
||||

|
||||
|
||||
## Run locally
|
||||
## License
|
||||
|
||||
[Install stack](https://docs.haskellstack.org/en/stable/install_and_upgrade/) and `stack run`.
|
||||
|
||||
## Usage example
|
||||
|
||||
Lines you should send are prefixed with `>` character, you should not type them.
|
||||
|
||||
Comments are prefixed with `--`, they are not part of transmissions.
|
||||
|
||||
`>` on its own means you need to press `return` - telnet should be configured to send it as CRLF.
|
||||
|
||||
1. Create simplex message queue:
|
||||
|
||||
```telnet
|
||||
>
|
||||
> abcd -- correlation ID, any string
|
||||
>
|
||||
> NEW 1234 -- 1234 is recipient's key
|
||||
|
||||
abcd
|
||||
|
||||
IDS QuCLU4YxgS7wcPFA YB4CCATREHkaQcEh -- recipient and sender IDs for the queue
|
||||
```
|
||||
|
||||
2. Sender can send their "key" to the queue:
|
||||
|
||||
```telnet
|
||||
> -- no signature (just press enter)
|
||||
> bcda -- correlation ID, any string
|
||||
> YB4CCATREHkaQcEh -- sender ID for the queue
|
||||
> SEND :key abcd
|
||||
|
||||
bcda
|
||||
YB4CCATREHkaQcEh
|
||||
OK
|
||||
```
|
||||
|
||||
3. Secure queue with sender's "key"
|
||||
|
||||
```telnet
|
||||
> 1234 -- recipient's "signature" - same as "key" in the demo
|
||||
> cdab
|
||||
> QuCLU4YxgS7wcPFA -- recipient ID
|
||||
> KEY abcd -- "key" provided by sender
|
||||
|
||||
cdab
|
||||
QuCLU4YxgS7wcPFA
|
||||
OK
|
||||
```
|
||||
|
||||
4. Sender can now send messages to the queue
|
||||
|
||||
```telnet
|
||||
> abcd -- sender's "signature" - same as "key" in the demo
|
||||
> dabc -- correlation ID
|
||||
> YB4CCATREHkaQcEh -- sender ID
|
||||
> SEND :hello
|
||||
|
||||
dabc
|
||||
YB4CCATREHkaQcEh
|
||||
OK
|
||||
```
|
||||
|
||||
5. Recipient recieves the message and acknowledges it to receive further messages
|
||||
|
||||
```telnet
|
||||
|
||||
-- no correlation ID for messages delivered without client command
|
||||
QuCLU4YxgS7wcPFA
|
||||
MSG ECA3w3ID 2020-10-18T20:19:36.874Z 5
|
||||
hello
|
||||
> 1234
|
||||
> abcd
|
||||
> QuCLU4YxgS7wcPFA
|
||||
> ACK
|
||||
|
||||
abcd
|
||||
QuCLU4YxgS7wcPFA
|
||||
OK
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||

|
||||
[AGPL v3](https://github.com/simplex-chat/simplexmq/blob/master/LICENSE)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module ChatOptions (getChatOpts, ChatOpts (..)) where
|
||||
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Agent.Transmission (SMPServer (..), smpServerP)
|
||||
import System.FilePath (combine)
|
||||
import System.Info (os)
|
||||
import Types
|
||||
|
||||
data ChatOpts = ChatOpts
|
||||
{ name :: Maybe ByteString,
|
||||
dbFileName :: String,
|
||||
smpServer :: SMPServer,
|
||||
termMode :: TermMode
|
||||
}
|
||||
|
||||
chatOpts :: FilePath -> Parser ChatOpts
|
||||
chatOpts appDir =
|
||||
ChatOpts
|
||||
<$> option
|
||||
(Just <$> str)
|
||||
( long "name"
|
||||
<> short 'n'
|
||||
<> metavar "NAME"
|
||||
<> help "optional name to use for invitations"
|
||||
<> value Nothing
|
||||
)
|
||||
<*> strOption
|
||||
( long "database"
|
||||
<> short 'd'
|
||||
<> metavar "DB_FILE"
|
||||
<> help ("sqlite database file path (" <> defaultDbFilePath <> ")")
|
||||
<> value defaultDbFilePath
|
||||
)
|
||||
<*> option
|
||||
parseSMPServer
|
||||
( long "server"
|
||||
<> short 's'
|
||||
<> metavar "SERVER"
|
||||
<> help "SMP server to use (smp.simplex.im:5223)"
|
||||
<> value (SMPServer "smp.simplex.im" (Just "5223") Nothing)
|
||||
)
|
||||
<*> option
|
||||
parseTermMode
|
||||
( long "term"
|
||||
<> short 't'
|
||||
<> metavar "TERM"
|
||||
<> help ("terminal mode: editor or basic (" <> termModeName deafultTermMode <> ")")
|
||||
<> value deafultTermMode
|
||||
)
|
||||
where
|
||||
defaultDbFilePath = combine appDir "smp-chat.db"
|
||||
deafultTermMode
|
||||
| os == "mingw32" = TermModeBasic
|
||||
| otherwise = TermModeEditor
|
||||
|
||||
parseSMPServer :: ReadM SMPServer
|
||||
parseSMPServer = eitherReader $ A.parseOnly (smpServerP <* A.endOfInput) . B.pack
|
||||
|
||||
parseTermMode :: ReadM TermMode
|
||||
parseTermMode = maybeReader $ \case
|
||||
"basic" -> Just TermModeBasic
|
||||
"editor" -> Just TermModeEditor
|
||||
_ -> Nothing
|
||||
|
||||
getChatOpts :: FilePath -> IO ChatOpts
|
||||
getChatOpts appDir = execParser opts
|
||||
where
|
||||
opts =
|
||||
info
|
||||
(chatOpts appDir <**> helper)
|
||||
( fullDesc
|
||||
<> header "Chat prototype using Simplex Messaging Protocol (SMP)"
|
||||
<> progDesc "Start chat with DB_FILE file and use SERVER as SMP server"
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChatTerminal
|
||||
( ChatTerminal (..),
|
||||
newChatTerminal,
|
||||
chatTerminal,
|
||||
updateUsername,
|
||||
ttyContact,
|
||||
ttyFromContact,
|
||||
)
|
||||
where
|
||||
|
||||
import ChatTerminal.Basic (getLn, putLn)
|
||||
import ChatTerminal.Core
|
||||
import ChatTerminal.POSIX
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (race_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Numeric.Natural
|
||||
import Styled
|
||||
import qualified System.Console.ANSI as C
|
||||
import Types
|
||||
|
||||
newChatTerminal :: Natural -> Maybe Contact -> TermMode -> IO ChatTerminal
|
||||
newChatTerminal qSize user termMode = do
|
||||
inputQ <- newTBQueueIO qSize
|
||||
outputQ <- newTBQueueIO qSize
|
||||
activeContact <- newTVarIO Nothing
|
||||
username <- newTVarIO user
|
||||
termSize <- fromMaybe (0, 0) <$> C.getTerminalSize
|
||||
let lastRow = fst termSize - 1
|
||||
termState <- newTVarIO $ newTermState user
|
||||
termLock <- newTMVarIO ()
|
||||
nextMessageRow <- newTVarIO lastRow
|
||||
threadDelay 500000 -- this delay is the same as timeout in getTerminalSize
|
||||
return ChatTerminal {inputQ, outputQ, activeContact, username, termMode, termState, termSize, nextMessageRow, termLock}
|
||||
|
||||
newTermState :: Maybe Contact -> TerminalState
|
||||
newTermState user =
|
||||
TerminalState
|
||||
{ inputString = "",
|
||||
inputPosition = 0,
|
||||
inputPrompt = promptString user
|
||||
}
|
||||
|
||||
chatTerminal :: ChatTerminal -> IO ()
|
||||
chatTerminal ct
|
||||
| termSize ct == (0, 0) || termMode ct == TermModeBasic =
|
||||
run basicReceiveFromTTY basicSendToTTY
|
||||
| otherwise = do
|
||||
initTTY
|
||||
updateInput ct
|
||||
run receiveFromTTY sendToTTY
|
||||
where
|
||||
run receive send = race_ (receive ct) (send ct)
|
||||
|
||||
basicReceiveFromTTY :: ChatTerminal -> IO ()
|
||||
basicReceiveFromTTY ct =
|
||||
forever $ getLn >>= atomically . writeTBQueue (inputQ ct)
|
||||
|
||||
basicSendToTTY :: ChatTerminal -> IO ()
|
||||
basicSendToTTY ct = forever $ readOutputQ ct >>= putLn
|
||||
|
||||
withTermLock :: ChatTerminal -> IO () -> IO ()
|
||||
withTermLock ChatTerminal {termLock} action = do
|
||||
_ <- atomically $ takeTMVar termLock
|
||||
action
|
||||
atomically $ putTMVar termLock ()
|
||||
|
||||
receiveFromTTY :: ChatTerminal -> IO ()
|
||||
receiveFromTTY ct@ChatTerminal {inputQ, activeContact, termSize, termState} =
|
||||
forever $
|
||||
getKey >>= processKey >> withTermLock ct (updateInput ct)
|
||||
where
|
||||
processKey :: Key -> IO ()
|
||||
processKey = \case
|
||||
KeyEnter -> submitInput
|
||||
key -> atomically $ do
|
||||
ac <- readTVar activeContact
|
||||
modifyTVar termState $ updateTermState ac (snd termSize) key
|
||||
|
||||
submitInput :: IO ()
|
||||
submitInput = do
|
||||
msg <- atomically $ do
|
||||
ts <- readTVar termState
|
||||
writeTVar termState $ ts {inputString = "", inputPosition = 0}
|
||||
let s = inputString ts
|
||||
writeTBQueue inputQ s
|
||||
return s
|
||||
withTermLock ct . printMessage ct $ styleMessage msg
|
||||
|
||||
sendToTTY :: ChatTerminal -> IO ()
|
||||
sendToTTY ct = forever $ do
|
||||
msg <- readOutputQ ct
|
||||
withTermLock ct $ do
|
||||
printMessage ct msg
|
||||
updateInput ct
|
||||
|
||||
readOutputQ :: ChatTerminal -> IO StyledString
|
||||
readOutputQ = atomically . readTBQueue . outputQ
|
||||
@@ -1,81 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module ChatTerminal.Basic where
|
||||
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Styled
|
||||
import System.Console.ANSI.Types
|
||||
import System.Exit (exitSuccess)
|
||||
import System.Terminal as C
|
||||
|
||||
getLn :: IO String
|
||||
getLn = withTerminal $ runTerminalT getTermLine
|
||||
|
||||
putLn :: StyledString -> IO ()
|
||||
putLn s =
|
||||
withTerminal . runTerminalT $
|
||||
putStyled s >> C.putLn >> flush
|
||||
|
||||
putStyled :: MonadTerminal m => StyledString -> m ()
|
||||
putStyled (s1 :<>: s2) = putStyled s1 >> putStyled s2
|
||||
putStyled (Styled [] s) = putString s
|
||||
putStyled (Styled sgr s) = setSGR sgr >> putString s >> resetAttributes
|
||||
|
||||
setSGR :: MonadTerminal m => [SGR] -> m ()
|
||||
setSGR = mapM_ $ \case
|
||||
Reset -> resetAttributes
|
||||
SetConsoleIntensity BoldIntensity -> setAttribute bold
|
||||
SetConsoleIntensity _ -> resetAttribute bold
|
||||
SetItalicized True -> setAttribute italic
|
||||
SetItalicized _ -> resetAttribute italic
|
||||
SetUnderlining NoUnderline -> resetAttribute underlined
|
||||
SetUnderlining _ -> setAttribute underlined
|
||||
SetSwapForegroundBackground True -> setAttribute inverted
|
||||
SetSwapForegroundBackground _ -> resetAttribute inverted
|
||||
SetColor l i c -> setAttribute . layer l . intensity i $ color c
|
||||
SetBlinkSpeed _ -> pure ()
|
||||
SetVisible _ -> pure ()
|
||||
SetRGBColor _ _ -> pure ()
|
||||
SetPaletteColor _ _ -> pure ()
|
||||
SetDefaultColor _ -> pure ()
|
||||
where
|
||||
layer = \case
|
||||
Foreground -> foreground
|
||||
Background -> background
|
||||
intensity = \case
|
||||
Dull -> id
|
||||
Vivid -> bright
|
||||
color = \case
|
||||
Black -> black
|
||||
Red -> red
|
||||
Green -> green
|
||||
Yellow -> yellow
|
||||
Blue -> blue
|
||||
Magenta -> magenta
|
||||
Cyan -> cyan
|
||||
White -> white
|
||||
|
||||
getTermLine :: MonadTerminal m => m String
|
||||
getTermLine = getChars ""
|
||||
where
|
||||
getChars s = awaitEvent >>= processKey s
|
||||
processKey s = \case
|
||||
Right (KeyEvent key ms) -> case key of
|
||||
CharKey c
|
||||
| ms == mempty || ms == shiftKey -> do
|
||||
C.putChar c
|
||||
flush
|
||||
getChars (c : s)
|
||||
| otherwise -> getChars s
|
||||
EnterKey -> do
|
||||
C.putLn
|
||||
flush
|
||||
pure $ reverse s
|
||||
BackspaceKey -> do
|
||||
moveCursorBackward 1
|
||||
eraseChars 1
|
||||
flush
|
||||
getChars $ if null s then s else tail s
|
||||
_ -> getChars s
|
||||
Left Interrupt -> liftIO exitSuccess
|
||||
_ -> getChars s
|
||||
@@ -1,130 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChatTerminal.Core where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.List (dropWhileEnd)
|
||||
import qualified Data.Text as T
|
||||
import SimplexMarkdown
|
||||
import Styled
|
||||
import System.Console.ANSI.Types
|
||||
import Types
|
||||
|
||||
data ChatTerminal = ChatTerminal
|
||||
{ inputQ :: TBQueue String,
|
||||
outputQ :: TBQueue StyledString,
|
||||
activeContact :: TVar (Maybe Contact),
|
||||
username :: TVar (Maybe Contact),
|
||||
termMode :: TermMode,
|
||||
termState :: TVar TerminalState,
|
||||
termSize :: (Int, Int),
|
||||
nextMessageRow :: TVar Int,
|
||||
termLock :: TMVar ()
|
||||
}
|
||||
|
||||
data TerminalState = TerminalState
|
||||
{ inputPrompt :: String,
|
||||
inputString :: String,
|
||||
inputPosition :: Int
|
||||
}
|
||||
|
||||
data Key
|
||||
= KeyLeft
|
||||
| KeyRight
|
||||
| KeyUp
|
||||
| KeyDown
|
||||
| KeyAltLeft
|
||||
| KeyAltRight
|
||||
| KeyCtrlLeft
|
||||
| KeyCtrlRight
|
||||
| KeyShiftLeft
|
||||
| KeyShiftRight
|
||||
| KeyEnter
|
||||
| KeyBack
|
||||
| KeyTab
|
||||
| KeyEsc
|
||||
| KeyChars String
|
||||
| KeyUnsupported
|
||||
deriving (Eq)
|
||||
|
||||
inputHeight :: TerminalState -> ChatTerminal -> Int
|
||||
inputHeight ts ct = length (inputPrompt ts <> inputString ts) `div` snd (termSize ct) + 1
|
||||
|
||||
updateTermState :: Maybe Contact -> Int -> Key -> TerminalState -> TerminalState
|
||||
updateTermState ac tw key ts@TerminalState {inputString = s, inputPosition = p} = case key of
|
||||
KeyChars cs -> insertCharsWithContact cs
|
||||
KeyTab -> insertChars " "
|
||||
KeyBack -> backDeleteChar
|
||||
KeyLeft -> setPosition $ max 0 (p - 1)
|
||||
KeyRight -> setPosition $ min (length s) (p + 1)
|
||||
KeyUp -> setPosition $ let p' = p - tw in if p' > 0 then p' else p
|
||||
KeyDown -> setPosition $ let p' = p + tw in if p' <= length s then p' else p
|
||||
KeyAltLeft -> setPosition prevWordPos
|
||||
KeyAltRight -> setPosition nextWordPos
|
||||
KeyCtrlLeft -> setPosition prevWordPos
|
||||
KeyCtrlRight -> setPosition nextWordPos
|
||||
KeyShiftLeft -> setPosition 0
|
||||
KeyShiftRight -> setPosition $ length s
|
||||
_ -> ts
|
||||
where
|
||||
insertCharsWithContact cs
|
||||
| null s && cs /= "@" && cs /= "/" =
|
||||
insertChars $ contactPrefix <> cs
|
||||
| otherwise = insertChars cs
|
||||
insertChars = ts' . if p >= length s then append else insert
|
||||
append cs = let s' = s <> cs in (s', length s')
|
||||
insert cs = let (b, a) = splitAt p s in (b <> cs <> a, p + length cs)
|
||||
contactPrefix = case ac of
|
||||
Just (Contact c) -> "@" <> B.unpack c <> " "
|
||||
Nothing -> ""
|
||||
backDeleteChar
|
||||
| p == 0 || null s = ts
|
||||
| p >= length s = ts' backDeleteLast
|
||||
| otherwise = ts' backDelete
|
||||
backDeleteLast = if null s then (s, 0) else let s' = init s in (s', length s')
|
||||
backDelete = let (b, a) = splitAt p s in (init b <> a, p - 1)
|
||||
setPosition p' = ts' (s, p')
|
||||
prevWordPos
|
||||
| p == 0 || null s = p
|
||||
| otherwise =
|
||||
let before = take p s
|
||||
beforeWord = dropWhileEnd (/= ' ') $ dropWhileEnd (== ' ') before
|
||||
in max 0 $ p - length before + length beforeWord
|
||||
nextWordPos
|
||||
| p >= length s || null s = p
|
||||
| otherwise =
|
||||
let after = drop p s
|
||||
afterWord = dropWhile (/= ' ') $ dropWhile (== ' ') after
|
||||
in min (length s) $ p + length after - length afterWord
|
||||
ts' (s', p') = ts {inputString = s', inputPosition = p'}
|
||||
|
||||
styleMessage :: String -> StyledString
|
||||
styleMessage = \case
|
||||
"" -> ""
|
||||
s@('@' : _) -> let (c, rest) = span (/= ' ') s in Styled selfSGR c <> markdown rest
|
||||
s -> markdown s
|
||||
where
|
||||
markdown :: String -> StyledString
|
||||
markdown = styleMarkdown . parseMarkdown . T.pack
|
||||
|
||||
updateUsername :: ChatTerminal -> Maybe Contact -> STM ()
|
||||
updateUsername ct a = do
|
||||
writeTVar (username ct) a
|
||||
modifyTVar (termState ct) $ \ts -> ts {inputPrompt = promptString a}
|
||||
|
||||
promptString :: Maybe Contact -> String
|
||||
promptString a = maybe "" (B.unpack . toBs) a <> "> "
|
||||
|
||||
ttyContact :: Contact -> StyledString
|
||||
ttyContact (Contact a) = Styled contactSGR $ B.unpack a
|
||||
|
||||
ttyFromContact :: Contact -> StyledString
|
||||
ttyFromContact (Contact a) = Styled contactSGR $ B.unpack a <> ">"
|
||||
|
||||
contactSGR :: [SGR]
|
||||
contactSGR = [SetColor Foreground Vivid Yellow]
|
||||
|
||||
selfSGR :: [SGR]
|
||||
selfSGR = [SetColor Foreground Vivid Cyan]
|
||||
@@ -1,102 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module ChatTerminal.POSIX where
|
||||
|
||||
import ChatTerminal.Core
|
||||
import Control.Concurrent.STM
|
||||
import Styled
|
||||
import qualified System.Console.ANSI as C
|
||||
import System.IO
|
||||
|
||||
initTTY :: IO ()
|
||||
initTTY = do
|
||||
hSetEcho stdin False
|
||||
hSetBuffering stdin NoBuffering
|
||||
hSetBuffering stdout NoBuffering
|
||||
|
||||
updateInput :: ChatTerminal -> IO ()
|
||||
updateInput ct@ChatTerminal {termSize, termState, nextMessageRow} = do
|
||||
C.hideCursor
|
||||
ts <- readTVarIO termState
|
||||
nmr <- readTVarIO nextMessageRow
|
||||
let (th, tw) = termSize
|
||||
ih = inputHeight ts ct
|
||||
iStart = th - ih
|
||||
prompt = inputPrompt ts
|
||||
(cRow, cCol) = relativeCursorPosition tw $ length prompt + inputPosition ts
|
||||
if nmr >= iStart
|
||||
then atomically $ writeTVar nextMessageRow iStart
|
||||
else clearLines nmr iStart
|
||||
C.setCursorPosition (max nmr iStart) 0
|
||||
putStr $ prompt <> inputString ts <> " "
|
||||
C.clearFromCursorToLineEnd
|
||||
C.setCursorPosition (iStart + cRow) cCol
|
||||
C.showCursor
|
||||
where
|
||||
clearLines :: Int -> Int -> IO ()
|
||||
clearLines from till
|
||||
| from >= till = return ()
|
||||
| otherwise = do
|
||||
C.setCursorPosition from 0
|
||||
C.clearFromCursorToLineEnd
|
||||
clearLines (from + 1) till
|
||||
|
||||
relativeCursorPosition :: Int -> Int -> (Int, Int)
|
||||
relativeCursorPosition width pos =
|
||||
let row = pos `div` width
|
||||
col = pos - row * width
|
||||
in (row, col)
|
||||
|
||||
printMessage :: ChatTerminal -> StyledString -> IO ()
|
||||
printMessage ChatTerminal {termSize, nextMessageRow} msg = do
|
||||
nmr <- readTVarIO nextMessageRow
|
||||
C.setCursorPosition nmr 0
|
||||
let (th, tw) = termSize
|
||||
lc <- printLines tw msg
|
||||
atomically . writeTVar nextMessageRow $ min (th - 1) (nmr + lc)
|
||||
where
|
||||
printLines :: Int -> StyledString -> IO Int
|
||||
printLines tw ss = do
|
||||
let s = styledToANSITerm ss
|
||||
ls
|
||||
| null s = [""]
|
||||
| otherwise = lines s <> ["" | last s == '\n']
|
||||
print_ ls
|
||||
return $ foldl (\lc l -> lc + (length l `div` tw) + 1) 0 ls
|
||||
|
||||
print_ :: [String] -> IO ()
|
||||
print_ [] = return ()
|
||||
print_ (l : ls) = do
|
||||
putStr l
|
||||
C.clearFromCursorToLineEnd
|
||||
putStr "\n"
|
||||
print_ ls
|
||||
|
||||
getKey :: IO Key
|
||||
getKey = charsToKey . reverse <$> keyChars ""
|
||||
where
|
||||
charsToKey = \case
|
||||
"\ESC" -> KeyEsc
|
||||
"\ESC[A" -> KeyUp
|
||||
"\ESC[B" -> KeyDown
|
||||
"\ESC[D" -> KeyLeft
|
||||
"\ESC[C" -> KeyRight
|
||||
"\ESCb" -> KeyAltLeft
|
||||
"\ESCf" -> KeyAltRight
|
||||
"\ESC[1;5D" -> KeyCtrlLeft
|
||||
"\ESC[1;5C" -> KeyCtrlRight
|
||||
"\ESC[1;2D" -> KeyShiftLeft
|
||||
"\ESC[1;2C" -> KeyShiftRight
|
||||
"\n" -> KeyEnter
|
||||
"\DEL" -> KeyBack
|
||||
"\t" -> KeyTab
|
||||
'\ESC' : _ -> KeyUnsupported
|
||||
cs -> KeyChars cs
|
||||
|
||||
keyChars cs = do
|
||||
c <- getChar
|
||||
more <- hReady stdin
|
||||
-- for debugging - uncomment this, comment line after:
|
||||
-- (if more then keyChars else \c' -> print (reverse c') >> return c') (c : cs)
|
||||
(if more then keyChars else return) (c : cs)
|
||||
@@ -1,250 +0,0 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import ChatOptions
|
||||
import ChatTerminal
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Reader
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent (getSMPAgentClient, runSMPAgentClient)
|
||||
import Simplex.Messaging.Agent.Client (AgentClient (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Client (smpDefaultConfig)
|
||||
import Simplex.Messaging.Util (raceAny_)
|
||||
import SimplexMarkdown
|
||||
import Styled
|
||||
import System.Directory (getAppUserDataDirectory)
|
||||
import System.Exit (exitFailure)
|
||||
import System.Info (os)
|
||||
import Types
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
{ tcpPort = undefined, -- TODO maybe take it out of config
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = "smp-chat.db",
|
||||
smpCfg = smpDefaultConfig
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
data ChatClient = ChatClient
|
||||
{ inQ :: TBQueue ChatCommand,
|
||||
outQ :: TBQueue ChatResponse,
|
||||
smpServer :: SMPServer,
|
||||
username :: TVar (Maybe Contact)
|
||||
}
|
||||
|
||||
-- | GroupMessage ChatGroup ByteString
|
||||
-- | AddToGroup Contact
|
||||
data ChatCommand
|
||||
= ChatHelp
|
||||
| AddContact Contact
|
||||
| AcceptContact Contact SMPQueueInfo
|
||||
| ChatWith Contact
|
||||
| SetName Contact
|
||||
| SendMessage Contact ByteString
|
||||
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
"/help" $> ChatHelp
|
||||
<|> "/add " *> (AddContact <$> contact)
|
||||
<|> "/accept " *> acceptContact
|
||||
<|> "/chat " *> chatWith
|
||||
<|> "/name " *> setName
|
||||
<|> "@" *> sendMessage
|
||||
where
|
||||
acceptContact = AcceptContact <$> contact <* A.space <*> smpQueueInfoP
|
||||
chatWith = ChatWith <$> contact
|
||||
setName = SetName <$> contact
|
||||
sendMessage = SendMessage <$> contact <* A.space <*> A.takeByteString
|
||||
contact = Contact <$> A.takeTill (== ' ')
|
||||
|
||||
data ChatResponse
|
||||
= ChatHelpInfo
|
||||
| Invitation SMPQueueInfo
|
||||
| Connected Contact
|
||||
| ReceivedMessage Contact ByteString
|
||||
| Disconnected Contact
|
||||
| YesYes
|
||||
| ErrorInput ByteString
|
||||
| ChatError AgentErrorType
|
||||
| NoChatResponse
|
||||
|
||||
serializeChatResponse :: Maybe Contact -> ChatResponse -> StyledString
|
||||
serializeChatResponse name = \case
|
||||
ChatHelpInfo -> chatHelpInfo
|
||||
Invitation qInfo -> "ask your contact to enter: /accept " <> showName name <> " " <> (bPlain . serializeSmpQueueInfo) qInfo
|
||||
Connected c -> ttyContact c <> " connected"
|
||||
ReceivedMessage c t -> ttyFromContact c <> " " <> msgPlain t
|
||||
Disconnected c -> "disconnected from " <> ttyContact c <> " - try \"/chat " <> bPlain (toBs c) <> "\""
|
||||
YesYes -> "you got it!"
|
||||
ErrorInput t -> "invalid input: " <> bPlain t
|
||||
ChatError e -> "chat error: " <> plain (show e)
|
||||
NoChatResponse -> ""
|
||||
where
|
||||
showName Nothing = "<your name>"
|
||||
showName (Just (Contact a)) = bPlain a
|
||||
msgPlain = styleMarkdown . parseMarkdown . decodeUtf8With onError
|
||||
onError _ _ = Just '?'
|
||||
|
||||
chatHelpInfo :: StyledString
|
||||
chatHelpInfo =
|
||||
"Using chat:\n\
|
||||
\/add <name> - create invitation to send out-of-band\n\
|
||||
\ to your contact <name>\n\
|
||||
\ (any unique string without spaces)\n\
|
||||
\/accept <name> <invitation> - accept <invitation>\n\
|
||||
\ (a string that starts from \"smp::\")\n\
|
||||
\ from your contact <name>\n\
|
||||
\/name <name> - set <name> to use in invitations\n\
|
||||
\@<name> <message> - send <message> (any string) to contact <name>\n\
|
||||
\ @<name> can be omitted to send to previous"
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
ChatOpts {dbFileName, smpServer, name, termMode} <- welcomeGetOpts
|
||||
let user = Contact <$> name
|
||||
t <- getChatClient smpServer user
|
||||
ct <- newChatTerminal (tbqSize cfg) user termMode
|
||||
-- setLogLevel LogInfo -- LogError
|
||||
-- withGlobalLogging logCfg $
|
||||
env <- newSMPAgentEnv cfg {dbFile = dbFileName}
|
||||
dogFoodChat t ct env
|
||||
|
||||
welcomeGetOpts :: IO ChatOpts
|
||||
welcomeGetOpts = do
|
||||
appDir <- getAppUserDataDirectory "simplex"
|
||||
opts@ChatOpts {dbFileName, termMode} <- getChatOpts appDir
|
||||
putStrLn "simpleX chat prototype"
|
||||
putStrLn $ "db: " <> dbFileName
|
||||
when (os == "mingw32") $ windowsWarning termMode
|
||||
putStrLn "type \"/help\" for usage information"
|
||||
pure opts
|
||||
|
||||
windowsWarning :: TermMode -> IO ()
|
||||
windowsWarning = \case
|
||||
m@TermModeBasic -> do
|
||||
putStrLn $ "running in Windows (terminal mode is " <> termModeName m <> ", no utf8 support)"
|
||||
putStrLn "it is recommended to use Windows Subsystem for Linux (WSL)"
|
||||
m -> do
|
||||
putStrLn $ "running in Windows, terminal mode " <> termModeName m <> " is not supported"
|
||||
exitFailure
|
||||
|
||||
dogFoodChat :: ChatClient -> ChatTerminal -> Env -> IO ()
|
||||
dogFoodChat t ct env = do
|
||||
c <- runReaderT getSMPAgentClient env
|
||||
raceAny_
|
||||
[ runReaderT (runSMPAgentClient c) env,
|
||||
sendToAgent t ct c,
|
||||
sendToChatTerm t ct,
|
||||
receiveFromAgent t ct c,
|
||||
receiveFromChatTerm t ct,
|
||||
chatTerminal ct
|
||||
]
|
||||
|
||||
getChatClient :: SMPServer -> Maybe Contact -> IO ChatClient
|
||||
getChatClient srv name = atomically $ newChatClient (tbqSize cfg) srv name
|
||||
|
||||
newChatClient :: Natural -> SMPServer -> Maybe Contact -> STM ChatClient
|
||||
newChatClient qSize smpServer name = do
|
||||
inQ <- newTBQueue qSize
|
||||
outQ <- newTBQueue qSize
|
||||
username <- newTVar name
|
||||
return ChatClient {inQ, outQ, smpServer, username}
|
||||
|
||||
receiveFromChatTerm :: ChatClient -> ChatTerminal -> IO ()
|
||||
receiveFromChatTerm t ct = forever $ do
|
||||
atomically (readTBQueue $ inputQ ct)
|
||||
>>= processOrError . A.parseOnly (chatCommandP <* A.endOfInput) . encodeUtf8 . T.pack
|
||||
where
|
||||
processOrError = \case
|
||||
Left err -> atomically . writeTBQueue (outQ t) . ErrorInput $ B.pack err
|
||||
Right ChatHelp -> atomically . writeTBQueue (outQ t) $ ChatHelpInfo
|
||||
Right (SetName a) -> atomically $ do
|
||||
let user = Just a
|
||||
writeTVar (username (t :: ChatClient)) user
|
||||
updateUsername ct user
|
||||
writeTBQueue (outQ t) YesYes
|
||||
Right cmd -> atomically $ writeTBQueue (inQ t) cmd
|
||||
|
||||
sendToChatTerm :: ChatClient -> ChatTerminal -> IO ()
|
||||
sendToChatTerm ChatClient {outQ, username} ChatTerminal {outputQ} = forever $ do
|
||||
atomically (readTBQueue outQ) >>= \case
|
||||
NoChatResponse -> return ()
|
||||
resp -> do
|
||||
name <- readTVarIO username
|
||||
atomically . writeTBQueue outputQ $ serializeChatResponse name resp
|
||||
|
||||
sendToAgent :: ChatClient -> ChatTerminal -> AgentClient -> IO ()
|
||||
sendToAgent ChatClient {inQ, smpServer} ct AgentClient {rcvQ} = do
|
||||
atomically $ writeTBQueue rcvQ ("1", "", SUBALL) -- hack for subscribing to all
|
||||
forever . atomically $ do
|
||||
cmd <- readTBQueue inQ
|
||||
writeTBQueue rcvQ `mapM_` agentTransmission cmd
|
||||
setActiveContact cmd
|
||||
where
|
||||
setActiveContact :: ChatCommand -> STM ()
|
||||
setActiveContact cmd =
|
||||
writeTVar (activeContact ct) $ case cmd of
|
||||
ChatWith a -> Just a
|
||||
SendMessage a _ -> Just a
|
||||
_ -> Nothing
|
||||
agentTransmission :: ChatCommand -> Maybe (ATransmission 'Client)
|
||||
agentTransmission = \case
|
||||
AddContact a -> transmission a $ NEW smpServer
|
||||
AcceptContact a qInfo -> transmission a $ JOIN qInfo $ ReplyVia smpServer
|
||||
ChatWith a -> transmission a SUB
|
||||
SendMessage a msg -> transmission a $ SEND msg
|
||||
ChatHelp -> Nothing
|
||||
SetName _ -> Nothing
|
||||
transmission :: Contact -> ACommand 'Client -> Maybe (ATransmission 'Client)
|
||||
transmission (Contact a) cmd = Just ("1", a, cmd)
|
||||
|
||||
receiveFromAgent :: ChatClient -> ChatTerminal -> AgentClient -> IO ()
|
||||
receiveFromAgent t ct c = forever . atomically $ do
|
||||
resp <- chatResponse <$> readTBQueue (sndQ c)
|
||||
writeTBQueue (outQ t) resp
|
||||
setActiveContact resp
|
||||
where
|
||||
chatResponse :: ATransmission 'Agent -> ChatResponse
|
||||
chatResponse (_, a, resp) = case resp of
|
||||
INV qInfo -> Invitation qInfo
|
||||
CON -> Connected contact
|
||||
END -> Disconnected contact
|
||||
MSG {m_body} -> ReceivedMessage contact m_body
|
||||
SENT _ -> NoChatResponse
|
||||
OK -> Connected contact -- hack for subscribing to all
|
||||
ERR e -> ChatError e
|
||||
where
|
||||
contact = Contact a
|
||||
setActiveContact :: ChatResponse -> STM ()
|
||||
setActiveContact = \case
|
||||
Connected a -> set $ Just a
|
||||
ReceivedMessage a _ -> set $ Just a
|
||||
Disconnected _ -> set Nothing
|
||||
_ -> return ()
|
||||
where
|
||||
set a = writeTVar (activeContact ct) a
|
||||
@@ -1,122 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module SimplexMarkdown 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 Styled
|
||||
import System.Console.ANSI.Types
|
||||
|
||||
data Markdown = Markdown Format Text | Markdown :|: Markdown
|
||||
deriving (Show)
|
||||
|
||||
data Format
|
||||
= Bold
|
||||
| Italic
|
||||
| Underline
|
||||
| StrikeThrough
|
||||
| Colored Color
|
||||
| NoFormat
|
||||
deriving (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
|
||||
|
||||
styleMarkdown :: Markdown -> StyledString
|
||||
styleMarkdown (s1 :|: s2) = styleMarkdown s1 <> styleMarkdown s2
|
||||
styleMarkdown (Markdown f s) = Styled sgr $ T.unpack s
|
||||
where
|
||||
sgr = case f of
|
||||
Bold -> [SetConsoleIntensity BoldIntensity]
|
||||
Italic -> [SetUnderlining SingleUnderline, SetItalicized True]
|
||||
Underline -> [SetUnderlining SingleUnderline]
|
||||
StrikeThrough -> [SetSwapForegroundBackground True]
|
||||
Colored c -> [SetColor Foreground Vivid c]
|
||||
NoFormat -> []
|
||||
|
||||
formats :: Map Char Format
|
||||
formats =
|
||||
M.fromList
|
||||
[ ('*', Bold),
|
||||
('_', Italic),
|
||||
('+', Underline),
|
||||
('~', StrikeThrough),
|
||||
('^', 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)
|
||||
]
|
||||
|
||||
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 [f] = f
|
||||
merge (f : fs) = foldl (:|:) f fs
|
||||
fragmentP :: Parser Markdown
|
||||
fragmentP =
|
||||
A.anyChar >>= \case
|
||||
' ' -> unmarked . (" " <>) <$> A.takeWhile (== ' ')
|
||||
c -> case M.lookup c formats of
|
||||
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 f s) <|> noFormat (T.singleton c <> p <> s)
|
||||
coloredP :: Parser Markdown
|
||||
coloredP = do
|
||||
color <- A.takeWhile (\c -> c /= ' ' && c /= '^')
|
||||
case M.lookup color colors of
|
||||
Just c ->
|
||||
let f = Colored c
|
||||
in (A.char ' ' *> formattedP '^' (color <> " ") f)
|
||||
<|> (A.char '^' $> Markdown f color)
|
||||
<|> noFormat ("^" <> color)
|
||||
_ -> noFormat ("^" <> color)
|
||||
unformattedP :: Char -> Parser Markdown
|
||||
unformattedP c = unmarked . (T.singleton 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
|
||||
@@ -1,29 +0,0 @@
|
||||
module Styled (StyledString (..), plain, bPlain, styledToANSITerm, styledToPlain) where
|
||||
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.String
|
||||
import System.Console.ANSI (SGR (..), setSGRCode)
|
||||
|
||||
data StyledString = Styled [SGR] String | StyledString :<>: StyledString
|
||||
|
||||
instance Semigroup StyledString where (<>) = (:<>:)
|
||||
|
||||
instance Monoid StyledString where mempty = plain ""
|
||||
|
||||
instance IsString StyledString where fromString = plain
|
||||
|
||||
plain :: String -> StyledString
|
||||
plain = Styled []
|
||||
|
||||
bPlain :: ByteString -> StyledString
|
||||
bPlain = Styled [] . B.unpack
|
||||
|
||||
styledToANSITerm :: StyledString -> String
|
||||
styledToANSITerm (Styled [] s) = s
|
||||
styledToANSITerm (Styled sgr s) = setSGRCode sgr <> s <> setSGRCode [Reset]
|
||||
styledToANSITerm (s1 :<>: s2) = styledToANSITerm s1 <> styledToANSITerm s2
|
||||
|
||||
styledToPlain :: StyledString -> String
|
||||
styledToPlain (Styled _ s) = s
|
||||
styledToPlain (s1 :<>: s2) = styledToPlain s1 <> styledToPlain s2
|
||||
@@ -1,14 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Types where
|
||||
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
|
||||
newtype Contact = Contact {toBs :: ByteString}
|
||||
|
||||
data TermMode = TermModeBasic | TermModeEditor deriving (Eq)
|
||||
|
||||
termModeName :: TermMode -> String
|
||||
termModeName = \case
|
||||
TermModeBasic -> "basic"
|
||||
TermModeEditor -> "editor"
|
||||
@@ -1,22 +1,17 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Simplex.Messaging.Agent (runSMPAgent)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Client (smpDefaultConfig)
|
||||
import Simplex.Messaging.Transport (TCP, Transport (..))
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
{ tcpPort = "5224",
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = "smp-agent.db",
|
||||
smpCfg = smpDefaultConfig
|
||||
}
|
||||
cfg = defaultAgentConfig {smpServers = L.fromList ["localhost:5223#bU0K+bRg24xWW//lS0umO1Zdw/SXqpJNtm1/RrPLViE="]}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
@@ -25,5 +20,4 @@ main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
|
||||
setLogLevel LogInfo -- LogError
|
||||
withGlobalLogging logCfg $
|
||||
runSMPAgent cfg
|
||||
withGlobalLogging logCfg $ runSMPAgent (transport @TCP) cfg
|
||||
|
||||
@@ -1,18 +1,277 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
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.Ini (Ini, lookupValue, readIniFile)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.X509 (PrivKey (PrivKeyRSA))
|
||||
import Network.Socket (ServiceName)
|
||||
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, storeLogFilePath)
|
||||
import Simplex.Messaging.Transport (ATransport (..), TCP, Transport (..))
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (combine)
|
||||
import System.IO (IOMode (..), hFlush, stdout)
|
||||
import Text.Read (readEither)
|
||||
|
||||
cfg :: ServerConfig
|
||||
cfg =
|
||||
defaultServerPort :: ServiceName
|
||||
defaultServerPort = "5223"
|
||||
|
||||
defaultBlockSize :: Int
|
||||
defaultBlockSize = 4096
|
||||
|
||||
serverConfig :: ServerConfig
|
||||
serverConfig =
|
||||
ServerConfig
|
||||
{ tcpPort = "5223",
|
||||
tbqSize = 16,
|
||||
{ tbqSize = 16,
|
||||
serverTbqSize = 128,
|
||||
msgQueueQuota = 256,
|
||||
queueIdBytes = 12,
|
||||
msgIdBytes = 6
|
||||
msgIdBytes = 6,
|
||||
-- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini
|
||||
transports = undefined,
|
||||
storeLog = undefined,
|
||||
blockSize = undefined,
|
||||
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"
|
||||
|
||||
iniFile :: FilePath
|
||||
iniFile = combine cfgDir "smp-server.ini"
|
||||
|
||||
defaultKeyFile :: FilePath
|
||||
defaultKeyFile = combine cfgDir "server_key"
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "Listening on port " ++ tcpPort cfg
|
||||
opts <- getServerOpts
|
||||
case serverCommand opts of
|
||||
ServerInit ->
|
||||
runExceptT (getConfig opts) >>= \case
|
||||
Right cfg -> do
|
||||
putStrLn "Error: server is already initialized. Start it with `smp-server start` command"
|
||||
printConfig cfg
|
||||
exitFailure
|
||||
Left _ -> do
|
||||
cfg <- initializeServer opts
|
||||
putStrLn "Server was initialized. Start it with `smp-server start` command"
|
||||
printConfig cfg
|
||||
ServerStart ->
|
||||
runExceptT (getConfig opts) >>= \case
|
||||
Right cfg -> runServer cfg
|
||||
Left e -> do
|
||||
putStrLn $ "Server is not initialized: " <> e
|
||||
putStrLn "Initialize server with `smp-server init` command"
|
||||
exitFailure
|
||||
ServerDelete -> do
|
||||
deleteServer
|
||||
putStrLn "Server key, config file and store log deleted"
|
||||
|
||||
getConfig :: ServerOpts -> ExceptT String IO ServerConfig
|
||||
getConfig opts = do
|
||||
ini <- readIni
|
||||
pk <- readKey ini
|
||||
storeLog <- liftIO $ openStoreLog opts ini
|
||||
pure $ makeConfig ini pk storeLog
|
||||
|
||||
makeConfig :: IniOpts -> C.FullPrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig
|
||||
makeConfig IniOpts {serverPort, blockSize, enableWebsockets} pk storeLog =
|
||||
let transports = (serverPort, transport @TCP) : [("80", transport @WS) | enableWebsockets]
|
||||
in serverConfig {serverPrivateKey = pk, storeLog, blockSize, transports}
|
||||
|
||||
printConfig :: ServerConfig -> IO ()
|
||||
printConfig ServerConfig {serverPrivateKey, storeLog} = do
|
||||
B.putStrLn $ "transport key hash: " <> serverKeyHash serverPrivateKey
|
||||
putStrLn $ case storeLog of
|
||||
Just s -> "store log: " <> storeLogFilePath s
|
||||
Nothing -> "store log disabled"
|
||||
|
||||
initializeServer :: ServerOpts -> IO ServerConfig
|
||||
initializeServer opts = do
|
||||
createDirectoryIfMissing False cfgDir
|
||||
ini <- createIni opts
|
||||
pk <- createKey ini
|
||||
storeLog <- openStoreLog opts ini
|
||||
pure $ makeConfig ini pk storeLog
|
||||
|
||||
runServer :: ServerConfig -> IO ()
|
||||
runServer cfg = do
|
||||
printConfig cfg
|
||||
forM_ (transports cfg) $ \(port, ATransport t) ->
|
||||
putStrLn $ "listening on port " <> port <> " (" <> transportName t <> ")"
|
||||
runSMPServer cfg
|
||||
|
||||
deleteServer :: IO ()
|
||||
deleteServer = do
|
||||
ini <- runExceptT readIni
|
||||
deleteIfExists iniFile
|
||||
case ini of
|
||||
Right IniOpts {storeLogFile, serverKeyFile} -> do
|
||||
deleteIfExists storeLogFile
|
||||
deleteIfExists serverKeyFile
|
||||
Left _ -> do
|
||||
deleteIfExists defaultKeyFile
|
||||
deleteIfExists defaultStoreLogFile
|
||||
|
||||
data IniOpts = IniOpts
|
||||
{ enableStoreLog :: Bool,
|
||||
storeLogFile :: FilePath,
|
||||
serverKeyFile :: FilePath,
|
||||
serverPort :: ServiceName,
|
||||
blockSize :: Int,
|
||||
enableWebsockets :: Bool
|
||||
}
|
||||
|
||||
readIni :: ExceptT String IO IniOpts
|
||||
readIni = do
|
||||
fileExists iniFile
|
||||
ini <- ExceptT $ readIniFile iniFile
|
||||
let enableStoreLog = (== Right "on") $ lookupValue "STORE_LOG" "enable" ini
|
||||
storeLogFile = opt defaultStoreLogFile "STORE_LOG" "file" ini
|
||||
serverKeyFile = opt defaultKeyFile "TRANSPORT" "key_file" ini
|
||||
serverPort = opt defaultServerPort "TRANSPORT" "port" ini
|
||||
enableWebsockets = (== Right "on") $ lookupValue "TRANSPORT" "websockets" ini
|
||||
blockSize <- liftEither . readEither $ opt (show defaultBlockSize) "TRANSPORT" "block_size" ini
|
||||
pure IniOpts {enableStoreLog, storeLogFile, serverKeyFile, serverPort, blockSize, enableWebsockets}
|
||||
where
|
||||
opt :: String -> Text -> Text -> Ini -> String
|
||||
opt def section key ini = either (const def) T.unpack $ lookupValue section key ini
|
||||
|
||||
createIni :: ServerOpts -> IO IniOpts
|
||||
createIni ServerOpts {enableStoreLog} = do
|
||||
writeFile iniFile $
|
||||
"[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"
|
||||
<> (if enableStoreLog then "" else "# ")
|
||||
<> "enable: on\n\
|
||||
\# file: "
|
||||
<> defaultStoreLogFile
|
||||
<> "\n\n\
|
||||
\[TRANSPORT]\n\n\
|
||||
\# key_file: "
|
||||
<> defaultKeyFile
|
||||
<> "\n\
|
||||
\# port: "
|
||||
<> defaultServerPort
|
||||
<> "\n\
|
||||
\# block_size: "
|
||||
<> show defaultBlockSize
|
||||
<> "\n\
|
||||
\websockets: on\n"
|
||||
pure
|
||||
IniOpts
|
||||
{ enableStoreLog,
|
||||
storeLogFile = defaultStoreLogFile,
|
||||
serverKeyFile = defaultKeyFile,
|
||||
serverPort = defaultServerPort,
|
||||
blockSize = defaultBlockSize,
|
||||
enableWebsockets = True
|
||||
}
|
||||
|
||||
readKey :: IniOpts -> ExceptT String IO C.FullPrivateKey
|
||||
readKey IniOpts {serverKeyFile} = do
|
||||
fileExists serverKeyFile
|
||||
liftIO (S.readKeyFile serverKeyFile) >>= \case
|
||||
[S.Unprotected (PrivKeyRSA pk)] -> pure $ C.FullPrivateKey pk
|
||||
[_] -> err "not RSA key"
|
||||
[] -> err "invalid key file format"
|
||||
_ -> err "more than one key"
|
||||
where
|
||||
err :: String -> ExceptT String IO b
|
||||
err e = throwE $ e <> ": " <> serverKeyFile
|
||||
|
||||
createKey :: IniOpts -> IO C.FullPrivateKey
|
||||
createKey IniOpts {serverKeyFile} = do
|
||||
(_, pk) <- C.generateKeyPair newKeySize
|
||||
S.writeKeyFile S.TraditionalFormat serverKeyFile [PrivKeyRSA $ C.rsaPrivateKey pk]
|
||||
pure pk
|
||||
|
||||
fileExists :: FilePath -> ExceptT String IO ()
|
||||
fileExists path = do
|
||||
exists <- liftIO $ doesFileExist path
|
||||
unless exists . throwE $ "file " <> path <> " not found"
|
||||
|
||||
deleteIfExists :: FilePath -> IO ()
|
||||
deleteIfExists path = doesFileExist path >>= (`when` removeFile path)
|
||||
|
||||
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 :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode))
|
||||
openStoreLog ServerOpts {enableStoreLog = l} IniOpts {enableStoreLog = l', storeLogFile = f}
|
||||
| l || l' = do
|
||||
createDirectoryIfMissing True logDir
|
||||
Just <$> openReadStoreLog f
|
||||
| otherwise = pure Nothing
|
||||
|
||||
data ServerOpts = ServerOpts
|
||||
{ serverCommand :: ServerCommand,
|
||||
enableStoreLog :: Bool
|
||||
}
|
||||
|
||||
data ServerCommand = ServerInit | ServerStart | ServerDelete
|
||||
|
||||
serverOpts :: Parser ServerOpts
|
||||
serverOpts =
|
||||
ServerOpts
|
||||
<$> subparser
|
||||
( command "init" (info (pure ServerInit) (progDesc "Initialize server: generate server key and ini file"))
|
||||
<> command "start" (info (pure ServerStart) (progDesc "Start server (ini: /etc/opt/simplex/smp-server.ini)"))
|
||||
<> command "delete" (info (pure ServerDelete) (progDesc "Delete server key, ini file and store log"))
|
||||
)
|
||||
<*> switch
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "enable store log for SMP queues persistence"
|
||||
)
|
||||
|
||||
getServerOpts :: IO ServerOpts
|
||||
getServerOpts = customExecParser p opts
|
||||
where
|
||||
p = prefs showHelpOnEmpty
|
||||
opts =
|
||||
info
|
||||
(serverOpts <**> helper)
|
||||
( fullDesc
|
||||
<> header "Simplex Messaging Protocol (SMP) Server"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ digraph SMPAgent {
|
||||
|
||||
subgraph clusterPersistence {
|
||||
graph [fontsize=11 color=gray]
|
||||
label="persistence (sqlite)\nQ: can multiple threads use it"
|
||||
label="persistence (sqlite)"
|
||||
connectionsStore [shape=cylinder label="duplex connections,\nSMP queues,\nrecent messages"]
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 506.94 131.5"><path d="m109.65 58.21-18.21-10.07-15.37 9.38-.19 9.59-7.47-4.92-10.16 6.19-.44-10.48-10.48-7 10-5.17c-.08 0 0 1-1.48-34.34l-23.65-11.39-32.2 10 7.19 34.63 10.81 8.37-8.22 3.9 5.37 26.1 7.55 7.09-5.4 3.29 4.19 20.18 16.94 17.94c.08-.1 1.78-1.41 21.79-17.32l-.58-13.71 8.58 7.28c.12-.12 1.84-1.39 17.56-13.89l.61-10.14 6.48 4.5c.13-.12 1.58-1.22 14.26-11.33z" fill="#231f20"/><path d="m94.54 69 15.11-10.79-18.21-10.07-15.37 9.38z"/><path d="m92.87 88.2 1.67-19.2-18.47-11.48-.38 18.75z" fill="#004b16"/><path d="m68.22 107.73v-19.89l-18.66-14.15 1.47 19.54z" fill="#004b16"/><path d="m68.22 87.84 18.74-13.43-18.55-12.22-18.85 11.5z"/><path d="m38.43 131.48-2.98-20.32-18.15-17.8 4.19 20.18z" fill="#004b16"/><path d="m35.45 111.16 23.91-17.06-18.53-15.09-23.53 14.35z"/><path d="m33.9 100.6-3.94-26.88-20.22-16.81 5.41 26.07z" fill="#004b16"/><path d="m29.96 73.72 27.85-15.82-20.8-13.94-27.27 12.95z"/><path d="m28.07 60.88-5.4-36.81-22.67-14.06 7.19 34.62z" fill="#004b16"/><path d="m22.67 24.07 33.14-12.7-23.61-11.37-32.2 10.01z"/><g fill="#1cb35c"><path d="m107.13 76.87c-14.02 11.13-14.33 11.33-14.26 11.33 1.74-20.1 1.59-19.2 1.67-19.2 16-11.45 15-10.79 15.11-10.79z"/><path d="m85.78 93.84c-17.35 13.8-17.63 13.89-17.56 13.89-.17-20.82-.07-19.89 0-19.89 20-14.3 18.67-13.43 18.74-13.43z"/><path d="m60.22 114.16c-21.66 17.22-21.86 17.32-21.79 17.32-3.07-20.94-3-20.32-3-20.32 25.47-18.16 23.86-17.06 23.93-17.06z"/><path d="m55.81 11.37c1.52 35.37 1.4 34.34 1.48 34.34-28.66 14.89-29.29 15.17-29.22 15.17-5.52-37.63-5.47-36.81-5.4-36.81z"/><path d="m57.81 57.9c1.15 26.81 1 25.88 1.11 25.88-24.81 16.67-25.09 16.82-25 16.82-4-27.58-4-26.88-3.94-26.88z"/></g><path xmlns="http://www.w3.org/2000/svg" d="m151.61 14.24 16.58-4v79.88q0 13.13 7.83 15.65-3.84 7.31-13.13 7.3-11.28 0-11.28-15.66z"/><path d="m186.66 111.72v-57.42h-9.08v-13.6h25.86v71zm8.56-98.54a9.62 9.62 0 1 1 -9.62 9.62 9.63 9.63 0 0 1 9.62-9.62z"/><path d="m262.94 111.74v-41.06c0-6.05-1.16-10.48-3.48-13.26s-6.11-4.18-11.37-4.18a17.74 17.74 0 0 0 -7.8 2.06 18 18 0 0 0 -6.46 5.1v51.34h-16.59v-71h11.94l3 6.64q6.76-8 20-8 12.66 0 20 7.59t7.33 21.19v43.58z"/><path d="m288.42 76.06q0-16.26 9.38-26.47t24.77-10.21q16.19 0 25.14 9.82t9 26.86q0 17-9.12 27t-25 10q-16.18 0-25.17-10.12t-9-26.88zm17.24 0q0 23.47 16.91 23.48a14.54 14.54 0 0 0 12.31-6.11q4.55-6.09 4.54-17.37 0-23.14-16.85-23.15a14.61 14.61 0 0 0 -12.33 6.09q-4.58 6.11-4.58 17.06z"/><path d="m412.37 111.74v-4.31c-1.37 1.5-3.71 2.82-7 3.95a31.33 31.33 0 0 1 -10.15 1.69q-14.87 0-23.38-9.42t-8.52-26.27q0-16.84 9.78-27.42a32 32 0 0 1 24.51-10.58 32.37 32.37 0 0 1 14.72 3.32v-28.46l16.58-4v101.5zm0-54.06a17.6 17.6 0 0 0 -11.07-4.24q-10 0-15.33 6.07t-5.37 17.41q0 22.15 21.36 22.15a16.11 16.11 0 0 0 5.87-1.42c2.32-1 3.83-1.92 4.54-2.89z"/><path d="m505.55 81.3h-50.74q.46 8.49 5.83 13.2t14.46 4.7q11.34 0 17.25-5.9l6.43 12.7q-8.75 7.09-26.13 7.1-16.25 0-25.7-9.52t-9.45-26.58q0-16.78 10.38-27.2a33.91 33.91 0 0 1 24.9-10.41q15.45 0 24.81 9.22t9.35 23.48a46.28 46.28 0 0 1 -1.39 9.21zm-50.15-12.47h34.89q-1.73-15.58-17.24-15.59-14.2 0-17.65 15.59z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,117 @@
|
||||
CREATE TABLE IF NOT EXISTS servers(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BLOB,
|
||||
PRIMARY KEY (host, port)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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, -- TODO add defauls here and below in the new schema
|
||||
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;
|
||||
|
||||
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, -- deprecated
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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,9 @@
|
||||
CREATE TABLE conn_confirmations (
|
||||
confirmation_id BLOB NOT NULL PRIMARY KEY,
|
||||
conn_alias BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
sender_key BLOB NOT NULL,
|
||||
sender_conn_info BLOB NOT NULL,
|
||||
accepted INTEGER NOT NULL,
|
||||
own_conn_info BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
) WITHOUT ROWID;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE messages ADD msg_body BLOB NOT NULL DEFAULT x''; -- this field replaces body TEXT
|
||||
-- TODO possibly migrate the data from body if it is possible in migration
|
||||
ALTER TABLE snd_messages ADD previous_msg_hash BLOB NOT NULL DEFAULT x'';
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE connections ADD conn_mode TEXT NOT NULL DEFAULT 'INV';
|
||||
|
||||
CREATE TABLE conn_invitations (
|
||||
invitation_id BLOB NOT NULL PRIMARY KEY,
|
||||
contact_conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
cr_invitation BLOB NOT NULL,
|
||||
recipient_conn_info BLOB NOT NULL,
|
||||
accepted INTEGER NOT NULL DEFAULT 0,
|
||||
own_conn_info BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
) WITHOUT ROWID;
|
||||
@@ -0,0 +1,9 @@
|
||||
# SQLite database migrations
|
||||
|
||||
These migrations are [embedded](../src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs) into the executable and run when SMP agent starts (as a separate executable or as a part of [simplex-chat](https://github.com/simplex-chat/simplex-chat) app).
|
||||
|
||||
Migration file names must have a format `YYYYMMDD-name.sql` - they will be executed in the order or lexicographic sorting of the names, the files with any other extension than `.sql` are ignored.
|
||||
|
||||
The proposed approach is to minimize the number of migrations and merge them together when possible, to align with the agent releases.
|
||||
|
||||
**Please note**: Adding or editing migrations will NOT update the migrations embedded into the executable, unless the [Migrations](../src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs) module is rebuilt - use `stack build --force-dirty` (in addition to edited files it seems to rebuild the files with TH splices and their dependencies, not all files as with `stack clean`).
|
||||
@@ -1,40 +1,64 @@
|
||||
name: simplex-messaging
|
||||
version: 0.1.0.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-messaging#readme
|
||||
name: simplexmq
|
||||
version: 0.5.2
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
|
||||
.
|
||||
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md SMP protocol>
|
||||
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md SMP agent protocol>
|
||||
.
|
||||
See <https://github.com/simplex-chat/simplex-chat terminal chat prototype> built with SimpleXMQ broker.
|
||||
|
||||
homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
license: AGPL-3
|
||||
author: Evgeny Poberezkin
|
||||
maintainer: evgeny@poberezkin.com
|
||||
copyright: 2020 Evgeny Poberezkin
|
||||
category: Web, System, Services, Cryptography
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020 simplex.chat
|
||||
category: Chat, Network, Web, System, Cryptography
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- CHANGELOG.md
|
||||
- migrations/*.*
|
||||
|
||||
dependencies:
|
||||
- ansi-terminal >= 0.10 && < 0.12
|
||||
- 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.*
|
||||
- composition == 1.0.*
|
||||
- constraints >= 0.12 && < 0.14
|
||||
- containers == 0.6.*
|
||||
- cryptonite >= 0.27 && < 0.30
|
||||
- direct-sqlite == 2.3.*
|
||||
- directory == 1.3.*
|
||||
- file-embed >= 0.0.14.0 && <= 0.0.15.0
|
||||
- filepath == 1.4.*
|
||||
- http-types == 0.12.*
|
||||
- generic-random >= 1.3 && < 1.5
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.15.*
|
||||
- mtl
|
||||
- mtl == 2.2.*
|
||||
- network == 3.1.*
|
||||
- network-transport == 0.5.*
|
||||
- QuickCheck == 2.14.*
|
||||
- random >= 1.1 && < 1.3
|
||||
- simple-logger == 0.1.*
|
||||
- sqlite-simple == 0.4.*
|
||||
- stm
|
||||
- template-haskell == 2.15.*
|
||||
- stm == 2.5.*
|
||||
- template-haskell == 2.16.*
|
||||
- text == 1.2.*
|
||||
- time == 1.9.*
|
||||
- transformers == 0.5.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.1.*
|
||||
- unliftio-core == 0.2.*
|
||||
- websockets == 0.12.*
|
||||
- x509 == 1.7.*
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
@@ -44,7 +68,10 @@ executables:
|
||||
source-dirs: apps/smp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
- cryptostore == 0.2.*
|
||||
- ini == 0.4.*
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
@@ -52,18 +79,7 @@ executables:
|
||||
source-dirs: apps/smp-agent
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
dog-food:
|
||||
source-dirs: apps/dog-food
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- ansi-terminal == 0.10.*
|
||||
- optparse-applicative == 0.15.*
|
||||
- simplex-messaging
|
||||
- terminal == 0.2.*
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
@@ -72,11 +88,12 @@ 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.14.*
|
||||
- timeit == 2.0.*
|
||||
|
||||
ghc-options:
|
||||
# - -haddock
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
# SMP agent protocol - duplex communication over SMP protocol
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [SMP agent](#smp-agent)
|
||||
- [SMP servers management](#smp-servers-management)
|
||||
- [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)
|
||||
- [INV message](#inv-message)
|
||||
- [ACK message](#ack-message)
|
||||
- [NEW message](#new-message)
|
||||
- [DEL message](#del-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)
|
||||
- [CONF notification and LET command](#conf-notification-and-let-command)
|
||||
- [REQ notification and ACPT command](#req-notification-and-acpt-command)
|
||||
- [INFO and CON notifications](#info-and-con-notifications)
|
||||
- [SUB command](#sub-command)
|
||||
- [SEND command and MID, SENT and MERR responses](#send-command-and-mid-sent-and-merr-responses)
|
||||
- [MSG notification](#msg-notification)
|
||||
- [END notification](#end-notification)
|
||||
- [OFF command](#off-command)
|
||||
- [DEL command](#del-command)
|
||||
- [Connection request](#connection-request)
|
||||
|
||||
## 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:
|
||||
- protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the agent protocol.
|
||||
- management of E2E encryption between SMP agents, generating ephemeral asymmetric keys for each connection.
|
||||
- SMP command authentication on SMP servers, generating ephemeral keys for each SMP queue.
|
||||
- TCP/TLS transport handshake with SMP servers.
|
||||
- validation of message integrity.
|
||||
|
||||
SMP agent protocol provides no encryption or security on the client side - it is assumed that the agent is executed in the trusted and secure environment, in one of three ways:
|
||||
- via TCP network using secure connection.
|
||||
- via local port (when the agent runs on the same device as a separate process).
|
||||
- via agent library, when the agent logic is included directly into the client application - [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) uses this approach.
|
||||
|
||||
## SMP agent
|
||||
|
||||
SMP agents communicate with each other via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) according to the commands received from its users. This protocol is a middle layer in SimpleX protocols (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 SMP queues that each connection consists of, and also the buffer of the most recent sent and received 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 hashes and IDs of the last received and sent messages.
|
||||
|
||||
## SMP servers management
|
||||
|
||||
SMP agent protocol commands do not contain the addresses of the SMP servers that the agent will use to create and use the connections (excluding the server address in queue URIs used in JOIN command). The list of the servers is a part of the agent configuration and can be dynamically changed by the agent implementation:
|
||||
- by the client applications via any API that is outside of scope of this protocol.
|
||||
- by the agents themselves based on availability and latency of the configured servers.
|
||||
|
||||
## SMP agent protocol components
|
||||
|
||||
SMP agent protocol has 3 main parts:
|
||||
|
||||
- the syntax and semantics of the messages that SMP agents exchange with each other in order to:
|
||||
- negotiate establishing unidirectional (simplex) encrypted queues on SMP servers.
|
||||
- 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 that are sent by the agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two or more 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 queue URIs) to protect [E2E encryption][1] from 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 consisting 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 SMP NEW command.
|
||||
2. Agent A creates an SMP connection 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 request](#connection-request).
|
||||
3. Alice sends the [connection request](#connection-request) to Bob via any secure channel (out-of-band message).
|
||||
4. Bob sends `JOIN` command with the connection request as a parameter to agent B to accept the connection.
|
||||
5. Establishing Alice's SMP queue (with SMP protocol commands):
|
||||
- Agent B sends an "SMP confirmation" with SMP SEND command to the SMP queue specified in the connection request - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info (profile, public key for E2E encryption, etc.). This message is encrypted using key passed in the connection request (or with the derived key, in which case public key for key derivation should be sent in clear text).
|
||||
- Agent A receives the SMP confirmation containing Bob's key and info as SMP MSG.
|
||||
- Agent A notifies Alice sending REQ notification with Bob's info.
|
||||
- Alice accepts connection request with ACPT command.
|
||||
- Agent A secures the queue with SMP KEY command.
|
||||
- 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 (SMP SEND command) with the connection request to this 2nd queue to Alice's agent (via the 1st queue) - this connection request SHOULD use "simplex" URI scheme.
|
||||
- Agent A, having received `REPLY` message, sends unauthenticated message (SMP SEND) to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info.
|
||||
- Bob's agent receives the key and Alice's information and secures the queue (SMP KEY).
|
||||
- Bob's agent sends the notification `INFO` with Alice's information to Bob.
|
||||
- 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 there are 4 steps (not including notifications):
|
||||
|
||||
1. Alice requests a new connection with `NEW` command and receives the invitation.
|
||||
2. Alice passes connection request out-of-band to Bob.
|
||||
3. Bob accepts the connection with `JOIN` command with the connection request to his agent.
|
||||
4. Alice accepts the connection with `ACPT` command.
|
||||
5. Both parties receive `CON` notification once duplex connection is established.
|
||||
|
||||
Clients SHOULD support establishing duplex connection asynchronously (when parties are intermittently offline) by persisting intermediate states and resuming SMP queue subscriptions.
|
||||
|
||||
## Communication between SMP agents
|
||||
|
||||
To establish duplex connections and to send messages on behalf of their clients, SMP agents communicate via SMP servers.
|
||||
|
||||
Agents 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 manage SMP queue rotation (`newQueueMessage`, `deleteQueueMsg`)
|
||||
- to manage encryption key rotation (TODO)
|
||||
- `msgPadding` - an optional message padding to make all SMP messages have constant size, to prevent servers from observing the actual message size.
|
||||
|
||||
### 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 ; here `agentMsgId` is sequential ID set by the sending agent
|
||||
agentMsgId = 1*DIGIT
|
||||
agentTimestamp = <date-time> ; RFC3339
|
||||
previousMsgHash = encoded
|
||||
encoded = <base64 encoded>
|
||||
|
||||
agentMessage = helloMsg / replyQueueMsg /
|
||||
clientMsg / invitationMsg/ acknowledgeMsg /
|
||||
newQueueMessage / deleteQueueMsg
|
||||
|
||||
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
|
||||
|
||||
replyQueueMsg = %s"REPLY" SP connectionRequest ; `connectionRequest` is defined below
|
||||
; this message can only be sent by the second connection party
|
||||
|
||||
clientMsg = %s"MSG" SP size CRLF clientMsgBody CRLF ; CRLF is in addition to CRLF in decryptedSmpMessageBody
|
||||
size = 1*DIGIT
|
||||
clientMsgBody = *OCTET
|
||||
|
||||
invitationMsg = %s"INV" SP connReqInvitation SP connInfo
|
||||
; `connReqInvitation` and `connInfo` are defined below
|
||||
|
||||
acknowledgeMsg = %s"ACK" SP agentMsgId SP msgHash SP ackStatus
|
||||
; NOT SUPPORTED in the current implementation
|
||||
|
||||
msgHash = encoded
|
||||
; base64 encoded hash of the received message
|
||||
|
||||
ackStatus = %s"OK" / ackError
|
||||
|
||||
ackError = %s"ERR" SP ackErrorType
|
||||
|
||||
ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr
|
||||
|
||||
ackUnknownMsg = %s"UNKNOWN"
|
||||
|
||||
ackProhibitedMsg = %s"PROHIBITED" ; unexpected message e.g. "HELLO" or "REPLY"
|
||||
|
||||
ackSyntaxErr = %s"SYNTAX" SP syntaxErrCode
|
||||
syntaxErrCode = 1*DIGIT ; TODO
|
||||
|
||||
newQueueMsg = %s"NEW" SP queueURI
|
||||
; this message can be sent by any party to add SMP queue to the connection.
|
||||
; NOT SUPPORTED in the current implementation
|
||||
|
||||
deleteQueueMsg = %s"DEL" SP queueURI
|
||||
; notification that the queue with passed URI will be deleted
|
||||
; no need to notify the other party about suspending queue separately, as suspended and deleted queues are indistinguishable to the sender
|
||||
; NOT SUPPORTED in the current implementation
|
||||
```
|
||||
|
||||
#### 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 MAY contain the public key that the recipient would use to verify messages signed by the sender.
|
||||
|
||||
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 SMP SEND command.
|
||||
|
||||
#### REPLY message
|
||||
|
||||
This is the message that is sent by the agent that received an out-of-band connection request to pass the connection request for the reply SMP queues 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.
|
||||
|
||||
#### INV message
|
||||
|
||||
This message is sent to the SMP queue(s) in `connReqContact`, to establish a new connection via existing unsecured queue, that acts as a permanent connection link of a user.
|
||||
|
||||
#### ACK message
|
||||
|
||||
This message is sent to confirm the client message reception. It includes received message number, message hash and the reception status.
|
||||
|
||||
#### NEW message
|
||||
|
||||
This message is sent to add an additional SMP queue to the connection. Unlike REPLY message it can be sent at any time.
|
||||
|
||||
#### DEL message
|
||||
|
||||
This message is sent to notify that the queue with passed URI will be deleted - having received this message, the receiving agent should no longer send messages to this queue. In case it was the only queue in the connection to which the agent could send the messages, it MAY also delete the reply queue(s) in the connection.
|
||||
|
||||
## 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 / letCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd
|
||||
agentMsg = invitation / confMsg / connReqMsg / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error
|
||||
|
||||
newCmd = %s"NEW" SP connectionMode [SP %s"NO_ACK"] ; response is `invitation` or `error`
|
||||
; NO_ACK parameter currently not supported
|
||||
|
||||
connectionMode = %s"INV" / %s"CON"
|
||||
|
||||
invitation = %s"INV" SP connectionRequest ; `connectionRequest` is defined below
|
||||
|
||||
confMsg = %s"CONF" SP confirmationId SP msgBody
|
||||
; msgBody here is any binary information identifying connection request
|
||||
|
||||
letCmd = %s"LET" SP confirmationId SP msgBody
|
||||
; msgBody here is any binary information identifying connecting party
|
||||
|
||||
confirmationId = 1*DIGIT
|
||||
|
||||
connReqMsg = %s"REQ" SP invitationId SP msgBody
|
||||
; msgBody here is any binary information identifying connection request
|
||||
|
||||
acceptCmd = %s"ACPT" SP invitationId SP msgBody
|
||||
; msgBody here is any binary information identifying connecting party
|
||||
|
||||
invitationId = 1*DIGIT
|
||||
|
||||
connInfo = %s"INFO" SP msgBody
|
||||
; msgBody here is any binary information identifying connecting party
|
||||
|
||||
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
|
||||
|
||||
connDown = %s"DOWN"
|
||||
; lost connection (e.g. because of Internet connectivity or server is down)
|
||||
|
||||
connUp = %s"UP"
|
||||
; restored connection
|
||||
|
||||
joinCmd = %s"JOIN" SP connectionRequest SP connInfo [SP %s"NO_REPLY"] [SP %s"NO_ACK"]
|
||||
; `connectionRequest` and `connInfo` are defined below
|
||||
; response is `connected` or `error`
|
||||
; parameters NO_REPLY and NO_ACK are currently not supported
|
||||
|
||||
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
|
||||
|
||||
messageId = %s"MID" SP agentMsgId
|
||||
|
||||
sent = %s"SENT" SP agentMsgId
|
||||
|
||||
messageError = %s"MERR" SP agentMsgId SP <errorType>
|
||||
|
||||
message = %s"MSG" SP msgIntegrity SP recipientMeta SP brokerMeta SP senderMeta SP binaryMsg
|
||||
recipientMeta = %s"R=" agentMsgId "," agentTimestamp ; receiving agent message metadata
|
||||
brokerMeta = %s"B=" brokerMsgId "," brokerTimestamp ; broker (server) message metadata
|
||||
senderMeta = %s"S=" agentMsgId "," agentTimestamp ; sending agent message metadata
|
||||
brokerMsgId = encoded
|
||||
brokerTimestamp = <date-time>
|
||||
msgIntegrity = ok / msgIntegrityError
|
||||
|
||||
msgIntegrityError = %s"ERR" SP msgIntegrityErrorType
|
||||
msgIntegrityErrorType = skippedMsgErr / badMsgIdErr / badHashErr
|
||||
|
||||
skippedMsgErr = %s"NO_ID" SP missingFromMsgId SP missingToMsgId
|
||||
badMsgIdErr = %s"ID" SP previousMsgId ; ID is lower than the previous
|
||||
badHashErr = %s"HASH"
|
||||
|
||||
missingFromMsgId = agentMsgId
|
||||
missingToMsgId = agentMsgId
|
||||
previousMsgId = agentMsgId
|
||||
|
||||
acknowledgeCmd = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R")
|
||||
|
||||
received = %s"RCVD" SP agentMsgId SP msgIntegrity
|
||||
; ID assigned by sending agent (in SENT response)
|
||||
; currently not implemented
|
||||
|
||||
msgStatus = ok | error
|
||||
|
||||
ok = %s"OK"
|
||||
|
||||
error = %s"ERR" SP <errorType>
|
||||
```
|
||||
|
||||
### Client commands and server responses
|
||||
|
||||
#### NEW command and INV response
|
||||
|
||||
`NEW` command is used to create a connection and a connection request to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party).
|
||||
|
||||
`INV` response is sent by the agent to the client of the initiating party.
|
||||
|
||||
`NEW` command has `connectionMode` parameter to define the connection mode - to be used to communicate with a single contact (invitation mode, `connectionMode` is `INV`) or to accept connection requests from anybody (contact mode, `connectionMode` is `CON`). The type of connection request is determined by `connectionMode` parameter.
|
||||
|
||||
#### JOIN command
|
||||
|
||||
It is used to create a connection and accept the connection request received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party).
|
||||
|
||||
#### CONF notification and LET command
|
||||
|
||||
When the joining party uses `JOIN` command to accept connection invitation created with `NEW INV` command, the initiating party will receive `CONF` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose.
|
||||
|
||||
To continue with the connection the initiating party should use `LET` command.
|
||||
|
||||
#### REQ notification and ACPT command
|
||||
|
||||
When the joining party uses `JOIN` command to connect to the contact created with `NEW CON` command, the initiating party will receive `REQ` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose.
|
||||
|
||||
To continue with the connection the party that created the contact should use `ACPT` command.
|
||||
|
||||
#### INFO and CON notifications
|
||||
|
||||
After the initiating party proceeds with the connection using `ACPT` command, the joining party will receive `INFO` notification that can be used to identify the initiating party or for any other purpose.
|
||||
|
||||
Once the connection is established and ready to accept client messages, both agents will send `CON` notification to their clients.
|
||||
|
||||
#### 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 MID, SENT, RCVD and MERR responses
|
||||
|
||||
`SEND` command is used by the client to send messages.
|
||||
|
||||
`MID` response with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server.
|
||||
|
||||
`SENT` notification is sent by the agent to confirm that the message was delivered to at least one of SMP servers. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session.
|
||||
|
||||
`RCVD` notification is sent by the agent when it receives `ACK` message from the receiving agent. This notification contains reception status, only one successful notification will be sent, and multiple error notifications will be sent in case `ACK` had error status.
|
||||
|
||||
In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted to this SMP queue.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### DOWN and UP notifications
|
||||
|
||||
These notifications are sent when server or network connection is, respectively, `DOWN` or back `UP`.
|
||||
|
||||
All the subscriptions made in the current client session will be automatically resumed when `UP` notification is received.
|
||||
|
||||
#### 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 request
|
||||
|
||||
Connection request `connectionRequest` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another connection request 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 request syntax:
|
||||
|
||||
```
|
||||
connectionRequest = connectionScheme "/" connReqType "#/?smp=" smpQueues "&e2e=" e2eEncryption
|
||||
connReqType = %s"invitation" / %s"contact"
|
||||
; this parameter has the same meaning as connectionMode in agent commands
|
||||
; `NEW INV` creates `invitation` connection request, `NEW CON` - `contact`
|
||||
connectionScheme = (%s"https://" clientAppServer) | %s"simplex:"
|
||||
clientAppServer = hostname [ ":" port ]
|
||||
; client app server, e.g. simplex.chat
|
||||
e2eEncryption = encryptionScheme ":" publicKey
|
||||
encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols,
|
||||
; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256)
|
||||
; will be replaced with double ratchet protocol and DH key exchange.
|
||||
publicKey = <base64url X509 SPKI key encoding>
|
||||
smpQueues = smpQueue [ "," 1*smpQueue ] ; SMP queues for the connection
|
||||
smpQueue = <URL-encoded queueURI defined in SMP protocol>
|
||||
```
|
||||
|
||||
All parameters are passed via URI hash to avoid sending them to the server (in case "https" scheme is used) - they can be used by the client-side code and processed by the client application. Parameters `smp` and `e2e` can be present in any order, any unknown additional parameters SHOULD be ignored.
|
||||
|
||||
`clientAppServer` is not an SMP server - it is a server that shows the instruction on how to download the client app that will connect using this connection request. This server can also host a mobile or desktop app manifest so that this link is opened directly in the app if it is installed on the device.
|
||||
|
||||
"simplex" URI scheme in `connectionProtocol` can be used instead of client app server, to connect without creating any web traffic. Client apps MUST support this URI scheme.
|
||||
|
||||
See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for syntax of `queueURI`.
|
||||
|
||||
[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,79 @@
|
||||
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: Bob's info and sender server key (SMP confirmation)
|
||||
note over BA: status: NONE/CONFIRMED
|
||||
activate BA
|
||||
AS ->> AA: MSG: Bob's info and<br>sender server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: REQ: connection request ID<br>and Bob's info
|
||||
A ->> AA: ACPT: accept connection request,<br>send Alice's info
|
||||
AA ->> 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
|
||||
AA ->> AS: ACK: confirm message
|
||||
|
||||
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 ->> AS: ACK: confirm message
|
||||
|
||||
AA ->> BS: SEND: Alice's info and sender's server key
|
||||
note over AA: status: ACTIVE/CONFIRMED
|
||||
activate AA
|
||||
BS ->> BA: MSG: Alice's info and<br>sender's server key
|
||||
note over BA: status: CONFIRMED/ACTIVE
|
||||
BA ->> B: INFO: Alice's info
|
||||
BA ->> BS: ACK: confirm message
|
||||
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
|
||||
BA ->> BS: ACK: confirm message
|
||||
|
||||
note over A, B: 8. notify users about connection success
|
||||
AA ->> A: CON: connected
|
||||
BA ->> B: CON: connected
|
||||
|
After Width: | Height: | Size: 35 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!
|
||||
|
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
|
||||
|
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)
|
||||
|
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
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,259 @@
|
||||
# SimpleX: messaging and application network
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [SimpleX objectives](#simplex-objectives)
|
||||
- [SimpleX network](#simplex-network)
|
||||
- [Threat model](#threat-model)
|
||||
|
||||
## SimpleX objectives
|
||||
|
||||
1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities:
|
||||
|
||||
- Security against passive and active (man-in-the-middle) attacks: the parties should have reliable end-to-end encryption and be able to identify and to some extent compensate for the presence of the active attacker who may modify, delete or add messages.
|
||||
|
||||
- Privacy: network server operators should have no ability to read or modify messages without detection, and it should be impossible or hard to infer the contacts the users communicate with. Non-malicious network operators should retain no record of participants communications.
|
||||
|
||||
- Reliability: the messages should be delivered even if some participating network servers or receiving clients fail, with “at least once” delivery guarantee.
|
||||
|
||||
- Integrity: the messages should be delivered without alteration, in the same order, and any unauthorized message change should be detected by the recipient.
|
||||
|
||||
- Asynchronous delivery: it should not be required that both communicating parties (client devices, services or applications) are online for reliable message delivery.
|
||||
|
||||
- Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency.
|
||||
|
||||
2. Provide better communication privacy, in particular meta-data privacy (who talks to whom and when), and security, in particular security against active attacks, than the alternative instant messaging solutions.
|
||||
|
||||
3. Balance user experience with privacy requirements, prioritizing experience of mobile device users.
|
||||
|
||||
## SimpleX network
|
||||
|
||||
### Overview
|
||||
|
||||
SimpleX network provides asynchronous messaging infrastructure that aims to deliver on the above objectives and to avoid the disadvantages of the alternative solutions.
|
||||
|
||||
The key differences of SimpleX network are:
|
||||
|
||||
- participants do not need to have globally unique addresses to communicate, instead they use redundant unidirectional (simplex) messaging queues, with a separate set of queues for each contact.
|
||||
|
||||
- connection requests are passed out-of-band, non-optionally protecting key exchange against man-in-the-middle attack.
|
||||
|
||||
- message queues provided by network servers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central servers and content/communication channels (requiring a separate type of server to host them, which is out of scope of this document).
|
||||
|
||||
- servers do not store any user information (no user profiles, contacts or messages, once they are delivered), and can use in-memory persistence only.
|
||||
|
||||
- users can change server provider(s) without losing their communication contacts, simply by changing the configuration on which servers the new queues are created.
|
||||
|
||||
SimpleX network has design similar to P2P networks, but unlike most P2P networks it consists of clients and servers, without dependency on any centralized component. Servers provide anonymous unidirectional message queues in order to:
|
||||
|
||||
- continuously accept messages for the recipients, even when they are offline.
|
||||
|
||||
- provide recipient anonymity, as the messaging queues do not identify the users, and different addresses of the same queue are used for senders and recipients to provide additional protection against server traffic correlation, in case transport connections are compromised.
|
||||
|
||||
Coincidentally, SimpleX network reminds [Pond messenger](https://github.com/agl/pond) design, the main differences are:
|
||||
|
||||
- Clients can receive messages via multiple servers (in Pond users use one home server and it serves as a user permanent address),
|
||||
|
||||
- Clients do not need to poll servers to receive messages, the messages from subscribed queues are delivered via an open transport connection as they are received by the servers, resulting in low latency of message delivery.
|
||||
|
||||
### SimpleX servers
|
||||
|
||||
Clients communicate with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides server authentication, confidentiality with forward secrecy, integrity and transport channel binding (to prevent SMP commands replay).
|
||||
|
||||
Users can use multiple servers. They choose the servers they use to receive the messages through, and they communicate with the servers chosen by the recipients to send messages.
|
||||
|
||||
It is assumed that users have some degree of trust to the servers, for one of the following reasons:
|
||||
|
||||
- They deploy and control the servers themselves from the available open-source code. The advantage of self-deployed servers is that users have full control of server information and activity, but the downside is that it is easier to correlate incoming and outgoing messages by observing server traffic, assuming small number of messages sent via user-hosted servers and irregular communication time. It can be mitigated by:
|
||||
|
||||
- sending noise traffic.
|
||||
|
||||
- future protocol versions can add configurable per queue message delivery delays (compromising on one of the design objectives of low latency).
|
||||
|
||||
- using SMP servers via an onion routing or mix network.
|
||||
|
||||
- They use servers from the trusted commercial providers. The more clients the provider has, the less meta-data about the communication times is leaked to the network observers, and even if transport confidentiality is compromised it is not possible to correlate sent and received traffic without compromising the server, because the clients use different queue IDs to send and receive messages, and the message delivered to the recipient is additionally encrypted by the server.
|
||||
|
||||
- To some extent, users trust their contacts and the servers they chose that the users send messages to. The client applications could further improve user trust to the servers chosen by their contacts by supporting either the list of servers that are “allowed to send to” or “prohibited to send to” (e.g., known trusted or compromised servers, certain IP ranges, server geographical locations etc.).
|
||||
|
||||
If users use the free servers deployed by the volunteers, there is a risk that the server code can maliciously record all queues and messages (even though encrypted) sent via the same transport connection and to gain a partial knowledge of the user’s communications graph and other meta-data. User clients can mitigate it by using some overlay network that protects the privacy of TCP connections, e.g., Tor, and by sending noise traffic between the end users.
|
||||
|
||||
The servers authorize users to access (send/receive/etc.) their message queues via a digital signature of transmissions using a unique key pair different for each queue sender and recipient, so no information that is required for the server to authorize users allows to aggregate user communications across multiple queues.
|
||||
|
||||
### Transport
|
||||
|
||||
SimpleX network does not rely on transport or overlay network for server or client authentication. SMP protocol itself must provide integrity, forward secure confidentiality of communicating parties and mutual authentication even if transport protocol (but not the server) is compromised by the active attacker.
|
||||
|
||||
To protect against MITM attack on the transport, the clients should verify server self-signed certificate during TLS handshake by validating its pre-shared hash of the certificate in the server address (either as part of client configuration or passed in SMP connection request).
|
||||
|
||||
Clients use TLS1.3 protocol to connect to the server, restricting supported cryptographic protocols as defined in SimpleX Messaging Protocol.
|
||||
|
||||
After TCP transport connection is established, and the server is authenticated, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size. When the actual message is smaller, it should be padded before encryption, and if it is larger, it can be split to chunks by the client application. The first blocks from the client and the server should include protocol version so that the party with the higher version can confirm compatibility and the transport channel identifier as described in SMP protocol.
|
||||
|
||||
Client can terminate the server transport connection without losing any messages – removal of the message from the server and the delivery of the next message happens when the client acknowledges the message reception (normally, after it is persisted in the client’s database).
|
||||
|
||||
Depending on the privacy requirements of the clients, the client can use a separate transport connection per queue, or receive messages from multiple queues via a single transport connection, for efficiency. In our implementations this configuration will be available to the end users.
|
||||
|
||||
### Server implementation
|
||||
|
||||
- does not store user access logs.
|
||||
|
||||
- does not store messages after the client has received them.
|
||||
|
||||
- permanently removes queues after they are deleted by the users
|
||||
|
||||
- only stores messages in transit in server operating memory; as clients are expected to use multiple servers to deliver each message, the message loss in one of the servers is acceptable.
|
||||
|
||||
### SimpleX clients and agents
|
||||
|
||||
Clients are assumed to be running on the trusted devices and can use the locally encrypted storage for messages, contacts, groups and other resources.
|
||||
|
||||
SimpleX clients implementing application-level protocols communicate with SimpleX servers via agents. SimpleX agents can be accessed in one of the following ways:
|
||||
|
||||
- via TCP network using trusted network connection or an overlay transport network.
|
||||
|
||||
- via local port (when the agent runs on the same device as a separate process).
|
||||
|
||||
- via agent library, when the agent logic is included directly into the client application.
|
||||
|
||||
The last option is the most secure, as it reduces the number of attack vectors in comparison with other options. The current implementation of SimpleX Chat uses this approach.
|
||||
|
||||
SimpleX agents provide the following operations:
|
||||
|
||||
- Creating and managing bi-directional (duplex) client connections and delivering messages via redundant groups of SimpleX queues.
|
||||
|
||||
- Exchanging keys and providing end-to-end encryption. The current prototype uses RSA cryptography with ad-hoc hybrid encryption scheme, we are currently switching it to X3DH for key exchange and double ratchet protocol for E2E encryption.
|
||||
|
||||
Communication between the client and the agent and the communication between the agents (via end-to-end encrypted messages delivered via SMP protocol) is described by SimpleX Agent Protocol.
|
||||
|
||||
Bi-directional client connections consist of multiple “send” and multiple “receive” queues (for redundancy) that agents use to send and receive messages; each queue should be used for limited amount of time, the rotation of queues is negotiated using SimpleX Agent Protocol.
|
||||
|
||||
There are two ways that SimpleX network users can establish the connection:
|
||||
|
||||
- Users communicate the initial connection invitation out-of-band, via link or via QR code. This out-of-band communication is assumed secure against active attacks and insecure against passive attacks. Once the connection is established that users are assumed to have confirmed out-of-band, the connection can no longer be compromised even if out-of-band invitation was obtained by the attacker. This out-of-band invitation does not contain any personal information, only the address of the queue and public key for the key exchange for end-to-end encryption.
|
||||
|
||||
- Users can create a “contact queue(s)” on SMP servers that would be used to receive connection requests. The important distinction with commonly used addressing systems is that this queue is only used to pass the connection request and not for subsequent messages. So, while it is possible to spam users who created such contact queues with connection requests, they can be removed without disrupting the communication with the existing contacts.
|
||||
|
||||
The reply queue addresses and the key exchange for the reply queues is negotiated via the direct queue.
|
||||
|
||||
### Message encryption
|
||||
|
||||
Messages are encrypted end-to-end using double ratchet protocol. Once the sending client deletes messages it won’t be able to decrypt them again. While messages are numbered within the queue, this only reveals to the servers how messages have been exchanged in a given queue that has a limited lifetime, so the total number of messages exchanged between contacts is not revealed.
|
||||
|
||||
The next queue to use is negotiated between agents in encrypted messages, and in most cases, it will be on another server (unless the client is configured to use one server, which is not recommended).
|
||||
|
||||
### Client storage
|
||||
|
||||
It is assumed that the client device and the storage is trusted, but clients can implement storage encryption using a user-provided passphrase.
|
||||
|
||||
## Threat model
|
||||
|
||||
### Assumptions
|
||||
|
||||
User protects their information and keys.
|
||||
|
||||
SimpleX Chat software is authentic.
|
||||
|
||||
User device is not compromised, excluding the scenarios below where it is explicitly stated it is compromised.
|
||||
|
||||
Used cryptographic primitives are not compromised.
|
||||
|
||||
### SimpleX Messaging Protocol server
|
||||
|
||||
**can:**
|
||||
|
||||
- learn when a queue recipient or sender is online via transmission times.
|
||||
|
||||
- correlate multiple queues to a single user in case they are accessed via the same transport connection or from the same IP address.
|
||||
|
||||
- correlate multiple queues to a single user based on the time they are accessed. Clients that use message notification servers can mitigate it by only subscribing to the queues after they receive the notifications and disconnecting after some time of inactivity.
|
||||
|
||||
- know how many messages is sent via the queue, clients can mitigate it with noise traffic.
|
||||
|
||||
- add, drop, duplicate or corrupt messages, but it will be detected by the client on SimpleX Agent Protocol level. Using multiple servers to deliver each message mitigates the loss of messages.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- decrypt messages
|
||||
|
||||
- compromise e2e encryption with MITM attack (as server is only used to pass one of two keys in DH exchange, the first key is passed out-of-band)
|
||||
|
||||
### A global passive adversary monitoring all Internet traffic
|
||||
|
||||
**can:**
|
||||
|
||||
- learn who is using SimpleX Chat.
|
||||
|
||||
- learn when messages are sent and received.
|
||||
|
||||
- learn which SimpleX Messaging Protocol servers are used.
|
||||
|
||||
- in case of low traffic on the servers, correlate senders and recipients by the time messages sent and received.
|
||||
|
||||
- observe the approximate size of the files transmitted – to O(log log M) of file size precision.
|
||||
|
||||
**cannot, even in case of compromised transport protocol:**
|
||||
|
||||
- correlate senders and recipients by the content of messages (as different queue IDs are used for senders and recipients, with additional encryption layer in the delivered messages).
|
||||
|
||||
### A local network attacker
|
||||
|
||||
**can:**
|
||||
|
||||
- observe when a user is using SimpleX Chat.
|
||||
|
||||
- block SimpleX Chat traffic.
|
||||
|
||||
- determine which servers the user communicates with.
|
||||
|
||||
- observe the approximate size of files transmissions and receptions.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- see who sends messages to the user and who the user sends the messages to
|
||||
|
||||
### An attacker who obtained user’s decrypted chat database
|
||||
|
||||
**can:**
|
||||
|
||||
- see the history of all messages
|
||||
|
||||
- see shared profiles of contacts and groups
|
||||
|
||||
- receive new user messages, but the user will detect it as the number and the hash of the previous message won’t match. Also, they will be instantly alerted that an unknown device is connected to their message queues, as long as they are online at the same time, as message queues only allow one active subscription.
|
||||
|
||||
- delete user’s message queues, so the contacts won’t be able to send messages.
|
||||
|
||||
- send messages from the user to their contacts, but the recipients will detect it as soon as the user sends the next message, because the previous message hash won’t match (and potentially won’t be able to decrypt them in case they don’t keep the previous ratchet keys).
|
||||
|
||||
**cannot:**
|
||||
|
||||
- impersonate a sender and send messages to the user whose database was stolen without also compromising the server (to place the message in the queue) or the user's device at a subsequent time (to place the message in the database).
|
||||
|
||||
### A user’s contact
|
||||
|
||||
**can:**
|
||||
|
||||
- spam the user with messages.
|
||||
|
||||
- forever retain messages from the user.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- prove to a third-party that a message came from a user (assuming a user’s device is not seized)
|
||||
|
||||
- prove that two contacts they have is the same user.
|
||||
|
||||
- two contacts cannot confirm if they are communicating with the same user.
|
||||
|
||||
### An attacker with Internet access
|
||||
|
||||
**can:**
|
||||
|
||||
- DoS SimpleX messaging servers.
|
||||
|
||||
- spam the user who created a “contact queue” (queue for accepting connection requests) by sending multiple connection requests.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- send messages to the user who they are not connected with.
|
||||
@@ -0,0 +1,736 @@
|
||||
# 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 queue URI](#smp-queue-uri)
|
||||
- [SMP procedure](#smp-procedure)
|
||||
- [SMP qualities and features](#smp-qualities-and-features)
|
||||
- [Cryptographic algorithms](#cryptographic-algorithms)
|
||||
- [Simplex queue IDs](#simplex-queue-ids)
|
||||
- [Server security requirements](#server-security-requirements)
|
||||
- [Message delivery notifications](#message-delivery-notifications)
|
||||
- [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)
|
||||
- [Enable notifications command](#enable-notifications-command)
|
||||
- [Acknowledge message delivery](#acknowledge-message-delivery)
|
||||
- [Suspend queue](#suspend-queue)
|
||||
- [Delete queue](#delete-queue)
|
||||
- [Sender commands](#sender-commands)
|
||||
- [Send message](#send-message)
|
||||
- [Notifier commands](#notifier-commands)
|
||||
- [Subscribe to queue notifications](#subscribe-to-queue-notifications)
|
||||
- [Server messages](#server-messages)
|
||||
- [Queue IDs response](#queue-ids-response)
|
||||
- [Deliver queue message](#deliver-queue-message)
|
||||
- [Notifier queue ID response](#notifier-queue-id-response)
|
||||
- [Deliver message notification](#deliver-message-notification)
|
||||
- [Subscription END notification](#subscription-end-notification)
|
||||
- [Error responses](#error-responses)
|
||||
- [OK response](#ok-response)
|
||||
- [Appendices](#appendices)
|
||||
- [Appendix A. Transport connection with the SMP server](#appendix-a)
|
||||
|
||||
## 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 analyzing 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 one or several [queue URIs](#smp-queue-uri) that parties can use to establish the initial connection, the encryption scheme and, it can include the public key(s) for end-to-end encryption.
|
||||
|
||||
The approach to out-of-band message passing and their syntax should be defined in application-level protocols.
|
||||
|
||||
## 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 queue URI
|
||||
|
||||
The SMP queue URIs MUST include server identity, queue hostname, an optional port, sender queue ID and the public key that the clients must use to verify responses. Server identity is used to establish secure connection protected from MITM attack with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol).
|
||||
|
||||
The [ABNF][8] syntax of the queue URI is:
|
||||
|
||||
```abnf
|
||||
queueURI = %s"smp://" smpServer "/" queueId "#" serverSignaturePublicKey
|
||||
; serverSignaturePublicKey syntax is defined below
|
||||
smpServer = serverIdentity "@" srvHost [":" port]
|
||||
srvHost = <hostname> ; RFC1123, RFC5891
|
||||
port = 1*DIGIT
|
||||
serverIdentity = base64url
|
||||
queueId = base64url
|
||||
base64url = <base64url encoded binary> ; RFC4648, section 5
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a))
|
||||
|
||||
## 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 verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
|
||||
|
||||
5. 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. This command contains unique "public" key `SK` previously generated by Bob.
|
||||
|
||||
2. From this moment the server will accept only signed commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has).
|
||||
|
||||
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 recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is provided when the queue is secured.
|
||||
|
||||
- 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 and servers must cryptographically sign commands, responses and messages for the following operations:
|
||||
|
||||
- With the recipient's key `RK` (server to verify):
|
||||
- create the queue (`NEW`)
|
||||
- subscribe to queue (`SUB`)
|
||||
- secure the queue (`KEY`)
|
||||
- enable queue notifications (`NKEY`)
|
||||
- acknowledge received messages (`ACK`)
|
||||
- suspend the queue (`OFF`)
|
||||
- delete the queue (`DEL`)
|
||||
- With the sender's key `SK` (server to verify):
|
||||
- send messages (`SEND`)
|
||||
- With the optional notifier's key:
|
||||
- subscribe to message notifications (`NSUB`)
|
||||
- With the server's key (for recipient and sender to verify)
|
||||
- queue IDs response (`IDS`)
|
||||
- notifier queue ID response (`NID`)
|
||||
- delivered messages (`MSG`)
|
||||
- `OK` and `ERR` responses (excluding error responses not related to a queue)
|
||||
|
||||
To sign/verify transmissions clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15].
|
||||
|
||||
To encrypt/decrypt message bodies delivered to the recipients, servers/clients MUST use x25519 or x448 algorithm defined in [RFC8709][15] to derive the shared secret (TODO encryption scheme).
|
||||
|
||||
Clients MUST encrypt message bodies sent via SMP servers - the protocol for this end-to-end encryption should be chosen by the clients using SMP protocol.
|
||||
|
||||
The reasons to use these algorithms:
|
||||
|
||||
- Faster operation than RSA algorithms.
|
||||
- DH key exchange provides forward secrecy.
|
||||
|
||||
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.
|
||||
|
||||
## Message delivery notifications
|
||||
|
||||
Supporting message delivery while the client mobile app is not running requires sending push notifications with the device token. All alternative mechanisms for background message delivery are unreliable, particularly on iOS platform. Obviously, supporting push notification delivery by simply subscribing to messages would reduce meta-data privacy as it allows to see all queues that a given device uses.
|
||||
|
||||
To protect the privacy of the recipients, there are several commands in SMP protocol that allow enabling and subscribing to message notifications from SMP queues, using separate set of "notifier keys" and via separate queue IDs - as long as SMP server is not compromised, these notifier queue IDs cannot be correlated with recipient or sender queue IDs.
|
||||
|
||||
The clients can optionally instruct a dedicated push notification server to subscribe to notifications and deliver push notifications to the device, which can then retrieve the messages in the background and send local notifications to the user - this is out of scope of SMP protocol. The commands that SMP protocol provides to allow it:
|
||||
|
||||
- `enableNotifications` (`"NKEY"`) with `notifierId` (`"NID"`) response - see [Enable notifications command](#enable-notifications-command).
|
||||
- `subscribeNotifications` (`"NSUB"`) - see [Subscribe to queue notifications](#subscribe-to-queue-notifications).
|
||||
- `messageNotification` (`"NMSG"`) - see [Deliver message notification](#deliver-message-notification).
|
||||
|
||||
## 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 = sessionIdentifier SP [corrId] SP [queueId] SP cmd ; corrId is required in client commands and server responses,
|
||||
; corrId is empty in server notifications.
|
||||
cmd = ping / recipientCmd / send / subscribeNotifications / serverMsg
|
||||
recipientCmd = create / subscribe / secure / enableNotifications /
|
||||
acknowledge / suspend / delete
|
||||
serverMsg = queueIds / message / notifierId / messageNotification /
|
||||
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 "send" before the queue is secured with secure command
|
||||
; signature is always empty with "ping" and "serverMsg"
|
||||
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 must 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 initial `send` commands) by verifying the client signatures. Command signature should be generated by applying the algorithm specified for the queue to the `signed` block of the transmission, using the key associated with the queue ID (recipient's, sender's or notifier's, depending on which queue ID is used).
|
||||
|
||||
### 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 `ok` response. This command should be sent unsigned and without queue ID.
|
||||
|
||||
```abnf
|
||||
ping = %s"PING"
|
||||
```
|
||||
|
||||
This command is always send unsigned.
|
||||
|
||||
### 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 recipientSignaturePublicKey SP recipientDhPublicKey
|
||||
recipientSignaturePublicKey = signaturePublicKey
|
||||
; the recipient's public key to verify commands for this queue
|
||||
|
||||
signaturePublicKey = signatureScheme ":" x509encoded
|
||||
signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448"
|
||||
; "rsa" means deprecated RSA-PSS signature scheme,
|
||||
; it must not be used for the new queues.
|
||||
|
||||
recipientDhPublicKey = dhPublicKey
|
||||
dhPublicKey = encryptionScheme ":" x509encoded
|
||||
; the recipient's key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt delivered message bodies
|
||||
|
||||
encryptionScheme = %s"x25519" | %s"x448"
|
||||
; TODO change to define the encryption scheme, e.g. "crypto_box"
|
||||
|
||||
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 and public keys to sign all responses and messages and to encrypt delivered message bodies:
|
||||
|
||||
```abnf
|
||||
queueIds = %s"IDS" SP recipientId SP senderId
|
||||
SP serverSignaturePublicKey SP serverDhPublicKey
|
||||
serverSignaturePublicKey = signatureKey
|
||||
; the server's public key to verify responses and messages for this queue
|
||||
serverDhPublicKey = dhPublicKey
|
||||
; the server's key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt delivered message bodies to the recipient
|
||||
recipientId = encoded
|
||||
senderId = encoded
|
||||
```
|
||||
|
||||
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 private part of the `recipientSignaturePublicKey` – this verifies that the client has the private key that will be used to sign subsequent commands for this queue.
|
||||
|
||||
`IDS` response transmission MUST be sent signed with `serverSignaturePublicKey` – this verifies that the server has the private key that will be used to sign subsequent responses and messages for this queue. This response should be sent with empty queue ID (the third part of 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)).
|
||||
|
||||
This transmission and its response MUST be signed.
|
||||
|
||||
#### Secure queue command
|
||||
|
||||
This command is sent by the recipient to the server to add sender's key to the queue:
|
||||
|
||||
```abnf
|
||||
secure = %s"KEY" SP senderSignaturePublicKey
|
||||
senderSignaturePublicKey = signaturePublicKey
|
||||
; the sender's key to verify SEND commands for this queue
|
||||
```
|
||||
|
||||
`senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command.
|
||||
|
||||
Once the queue is secured only signed messages can be sent to it.
|
||||
|
||||
#### Enable notifications command
|
||||
|
||||
This command is sent by the recipient to the server to add notifier's key to the queue, to allow push notifications server to receive notifications when the message arrives, via a separate queue ID, without receiving message content.
|
||||
|
||||
```abnf
|
||||
enableNotifications = %s"NKEY" SP notifierKey
|
||||
notifierKey = signatureScheme ":" x509encoded ; the notifier's public key public key to verify NSUB command for this queue
|
||||
```
|
||||
|
||||
The server will respond with `notifierId` response if notifications were enabled and the notifier's key was successfully added to the queue:
|
||||
|
||||
```abnf
|
||||
notifierId = %s"NID" SP notifierId
|
||||
recipientId = encoded
|
||||
```
|
||||
|
||||
This response is sent with the recipient's queue ID (the third part of the transmission).
|
||||
|
||||
To receive the message notifications, `subscribeNotifications` command ("NSUB") must be sent signed with the notifier's key.
|
||||
|
||||
#### 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 = signatureScheme ":" x509encoded ; the sender's public key to sign SEND commands 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.
|
||||
|
||||
### Notifier commands
|
||||
|
||||
#### Subscribe to queue notifications
|
||||
|
||||
The push notifications server (notifier) must use this command to start receiving message notifications from the queue:
|
||||
|
||||
```abnf
|
||||
subscribeNotifications = %s"NSUB"
|
||||
```
|
||||
|
||||
If subscription is successful the server must respond with `ok` response if no messages are available. The notifier will be receiving the message notifications from this queue until the transport connection is closed or until another transport connection subscribes to notifications from the same simplex queue - in this case the first subscription should be cancelled and [subscription END notification](#subscription-end-notification) delivered.
|
||||
|
||||
The first message notification will be delivered either immediately or as soon as the message is available.
|
||||
|
||||
### 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 encryptedMessage
|
||||
encryptedMessage = <encrypt sentMessage>
|
||||
sentMessage = 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]
|
||||
|
||||
`msgBody` - see syntax in [Send message](#send-message)
|
||||
|
||||
When server delivers the messages to the recipient, message body should be encrypted with the secret derived from DH exchange using the keys passed during the queue creation and returned with `queueIds` response.
|
||||
|
||||
This is done to prevent the possibility of correlation of incoming and outgoing traffic of SMP server inside transport protocol.
|
||||
|
||||
#### Notifier queue ID response
|
||||
|
||||
Server must respond with this message when queue notifications are enabled.
|
||||
|
||||
See its syntax in [Enable notifications command](#enable-notifications-command)
|
||||
|
||||
#### Deliver message notification
|
||||
|
||||
The server must deliver message notifications to all simplex queues that were subscribed with `subscribeNotifications` command ("NSUB") on the currently open transport connection. The syntax for the message notification delivery is:
|
||||
|
||||
```abnf
|
||||
messageNotification = %s"NMSG"
|
||||
```
|
||||
|
||||
Message notification does not contain any message data or meta-data, it only notifies that the message is available.
|
||||
|
||||
#### 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](#send-message) command).
|
||||
- message queue quota exceeded error (`QUOTA`) - too many messages were sent to the message queue. Further messages can only be sent after the recipient retrieves the messages.
|
||||
- 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.
|
||||
|
||||
For scenarios when meta-data privacy is critical, it is recommended that clients:
|
||||
- communicating over Tor network,
|
||||
- establish a separate connection for each SMP queue,
|
||||
- send noise traffic (using PING command).
|
||||
|
||||
In addition to that, the servers can be deployed as Tor onion services.
|
||||
|
||||
The transport protocol should provide the following:
|
||||
- server authentication (by matching server certificate hash with `serverIdentity`),
|
||||
- forward secrecy (by encrypting the traffic using ephemeral keys agreed during transport handshake),
|
||||
- integrity (preventing data modification by the attacker without detection),
|
||||
- unique channel binding (`sessionIdentifier`) to include in the signed part of SMP transmissions.
|
||||
|
||||
By default, the client and server communicate using [TLS 1.3 protocol][13] restricted to:
|
||||
- TLS_AES_256_GCM_SHA384 cypher suite,
|
||||
- ed25519 and ed448 EdDSA algorithms for signatures,
|
||||
- x25519 and x448 ECDHE groups for key exchange.
|
||||
- servers must send only one self-signed certificate in the handshake, clients must abort the connection in case more than one certificate is sent.
|
||||
- server and client TLS configuration should not allow resuming the sessions.
|
||||
|
||||
During TLS handshake the client must validate that the hash of the server certificate SPKI block is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection.
|
||||
|
||||
Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes).
|
||||
|
||||
The first block sent by the client should be `clientHello` and the server should respond with `serverHello`:
|
||||
|
||||
```abnf
|
||||
clientHello = SP smpVersion SP reserved pad
|
||||
serverHello = sessionIdentifier SP smpVersion SP reserved pad
|
||||
sessionIdentifier = <base64 encoded> ; unique session identifier derived from transport connection handshake
|
||||
; it should be included in all SMP transmissions sent in this transport connection.
|
||||
|
||||
smpVersion = %s"SMP v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ; semver format, the version in this document is v0.5.0
|
||||
reserved = <reserved for additional information>
|
||||
pad = 1*OCTET
|
||||
```
|
||||
|
||||
For TLS 1.3 transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol.
|
||||
|
||||
The communication party (client or server) that has the lower protocol version should assume that this version will be supported by another party, the party with the higher protocol version should abort the connection in case they cannot support the lower version.
|
||||
|
||||
[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
|
||||
[13]: https://datatracker.ietf.org/doc/html/rfc8446
|
||||
[14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3
|
||||
[15]: https://www.rfc-editor.org/rfc/rfc8709.html
|
||||
@@ -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,52 @@
|
||||
# Open connections
|
||||
|
||||
## Problem
|
||||
|
||||
This proposal describes how to create invitations that can be used multiple times.
|
||||
|
||||
It can be used for:
|
||||
- an open invitation to join a group.
|
||||
- an open invitation to connect to a person - e.g. QR code/invitation link on a person's website, or passed from one person to another.
|
||||
- part of the solution for public DNS-based addresses (when a directory server would map address in some domain name#example.tld to an open invitation).
|
||||
|
||||
## Solution
|
||||
|
||||
No changes to SMP protocol - a dedicated unsecured SMP queue is used to receive invitations to connect that are sent in encrypted agent message. An unsecured SMP queue is used as an out-of-band channel for establishing another SMP queue.
|
||||
|
||||
Additional parameters in commands in SMP agent protocol:
|
||||
|
||||
- `NEW` command will have a parameter `INV` or `CON` to create an invitation or a permanent contact connection.
|
||||
|
||||
`conn_alias? OPEN` (or `PUB`, `NEWPUB`, tbc) - to create an "open"/"public" queue, the response is an invitation in a different format (TBC):
|
||||
- should allow multiple servers (probably the original invitation should be extended to support it)
|
||||
- should have a marker to indicate it's an open/public queue (probably the original invitation should be extended to include an invitation type).
|
||||
|
||||
e.g. `smp:<queue_type>::<server1>/<queue_id1>,<server2>/<queue_id2>::<key1>`
|
||||
|
||||
`queue_type`:
|
||||
- `prv` - original invitation, should be accepted with KEY SMP message
|
||||
- `pub` - open invitation, should be accepted with INV SMP message (to be added to SMP protocol)
|
||||
|
||||
```mmd
|
||||
A ->> AA: oidA? OPEN
|
||||
AA ->> A: oidA INV pub_inv
|
||||
|
||||
...
|
||||
|
||||
B ->> BA: cidBA? JOIN pub_inv len CRLF meta_binary CRLF ; change command to require meta, len can be 0 for the current usage ; meta is used to send user profile
|
||||
BA ->> B: cidBA OK
|
||||
BA ->> SA ->> AA: INV prv_inv CRLF meta_binary
|
||||
AA ->> A: oidA CONF invID len meta_binary
|
||||
A ->> AA cidAB? LET invID
|
||||
|
||||
establish connection as usual
|
||||
|
||||
BA ->> B: cidBA CON
|
||||
AA ->> A: cidAB CON
|
||||
```
|
||||
|
||||
That protocol requires addressing the current problem when an invitation cannot be accepted when the party that generated the invitation is not online.
|
||||
|
||||
Questions.
|
||||
|
||||
1. Do we need to differentiate the semantics of the invitation on the syntax level, or should we allow to just manage it outside of protocol when the receiving agent decides which SMP messages to accept and which to ignore (KEY / INV).
|
||||
@@ -16,49 +16,60 @@ For initial implementation I propose approach to be as simple as possible as lon
|
||||
|
||||
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 the session key that will be established during the handshake.
|
||||
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 key, 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) and additional server ID (256 bits) in advance in order to be able to establish connection.
|
||||
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 could be the following:
|
||||
The handshake sequence is the following:
|
||||
|
||||
1. Once the connection is established, the server sends its public key 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.
|
||||
3. If the hash is the same, the client should generate a random symmetric AES key and IV that will be used as a session key both by the client and the server.
|
||||
4. The client then should encrypt this symmetric key with the public key that the server sent and send back to the server the result and the server ID also shared with the client in advance: `rsa-encrypt(aes-key, iv, server-id)`.
|
||||
5. The server should decrypt the received key, IV and server id with its private key.
|
||||
6. The server should compare the `server-id` sent by the client and if it does not match its ID terminate the connection.
|
||||
7. In case of successful decryption and matching server ID, the server should send encrypted welcome header.
|
||||
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.
|
||||
|
||||
```abnf
|
||||
aes_welcome_header = aes_header_auth_tag aes_encrypted_header
|
||||
welcome_header = smp_version ["," smp_mode] *SP ; decrypt(aes_encrypted_header) - 32 bytes
|
||||
smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format
|
||||
; for example: v123.456.789-alpha.7
|
||||
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
|
||||
aes_header_auth_tag = aes_auth_tag
|
||||
aes_auth_tag = 16*16(OCTET)
|
||||
```
|
||||
|
||||
No payload should follow this header, it is only used to confirm successful handshake and send the SMP protocol version that the server supports.
|
||||
|
||||
All the subsequent data both from the client and from the server should be sent encrypted using symmetric AES key and IV sent by the client during the handshake.
|
||||
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
|
||||
transport_block = aes_header_auth_tag aes_encrypted_header aes_body_auth_tag aes_encrypted_body
|
||||
aes_encrypted_header = 32*32(OCTET)
|
||||
header = padded_body_size payload_size reserved ; decrypt(aes_encrypted_header) - 32 bytes
|
||||
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
|
||||
body = payload pad
|
||||
padded_body_size = size ; body size in bytes
|
||||
payload_size = size ; payload_size in bytes
|
||||
size = 4*4(OCTET)
|
||||
reserved = 24*24(OCTET)
|
||||
aes_body_auth_tag = aes_auth_tag
|
||||
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
|
||||
@@ -105,7 +116,9 @@ Symmetric keys are generated per message and encrypted with receiver's public ke
|
||||
The syntax of each encrypted message body is the following:
|
||||
|
||||
```abnf
|
||||
encrypted_message_body = rsa_encrypted_header aes_encrypted_body
|
||||
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)
|
||||
|
||||
@@ -120,7 +133,6 @@ body = payload pad
|
||||
|
||||
Future considerations:
|
||||
- Generation of symmetric keys per session and session rotation;
|
||||
- Signature and verification of messages.
|
||||
|
||||
## E2E implementation
|
||||
|
||||
@@ -133,9 +145,9 @@ For each message, the agent performs the following actions to encrypt it:
|
||||
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)
|
||||
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.
|
||||
|
||||
@@ -143,7 +155,7 @@ Or we could use this algorithm to allow some data that is sent e2e in clear text
|
||||
|
||||
### Message decryption
|
||||
|
||||
Agent decrypt the message following the same steps in the opposite order
|
||||
Agent decrypts 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).
|
||||
@@ -0,0 +1,135 @@
|
||||
sequenceDiagram
|
||||
participant M as Existing<br>member (M)
|
||||
participant MA as Existing<br>member<br>agent (MA)
|
||||
participant A as Alice (A)
|
||||
participant AA as Alice's<br>agent (AA)
|
||||
participant BA as Bob's<br>agent (BA)
|
||||
participant B as Bob (B)
|
||||
|
||||
note over A, AA: 1. create new group (no members)
|
||||
A ->> AA: G:gidA? NEW<br>(gidA - group ID of this group for A,<br>can be generated by the agent)
|
||||
|
||||
note over AA: create "internal" broadcast associated with the group<br>(B: NEW)
|
||||
|
||||
AA ->> A: G:gidA OK
|
||||
|
||||
note over A, BA: 2. add Bob to group
|
||||
|
||||
A ->> AA: C:idAB INTRO G:gidA gInfo<br>(idAB - conn alias A has for B)
|
||||
|
||||
note over AA: generate new random ID for member B (midB, unique per group)<br>initiate "internal" connection gidAB for B in group<br>(internal means that it is not visible to the clients<br>and cannot be used with client commands)
|
||||
|
||||
AA ->> BA: via idAB: GROUP C:midB g:gInvAB gInfo
|
||||
BA ->> B: C:idBA REQ G:invID gInfo<br>(invID - to refer to it in ACPT)
|
||||
B ->> BA: G:gidB? ACPT G:invID<br>(RJCT G:invID could be added)
|
||||
|
||||
note over BA: create group and "internal" broadcast associated with the group<br>(B: NEW)
|
||||
|
||||
BA ->> B: G:gidB OK
|
||||
|
||||
BA ->> AA: establish internal connection gidBA (using g:gInvAB) for A in group
|
||||
|
||||
note over BA, AA: add connections gidBA and gidAB to broadcasts<br>(B: ADD)
|
||||
|
||||
AA ->> A: G:gidA CON C:idAB
|
||||
BA ->> B: G:gidA CON C:idBA
|
||||
|
||||
note over M, B: For each existing member M:<br>create and accept internal introduction between connections, related to the group, via gidAB/BA/AM/MA, connections created are gidBM and gidMB<br>The fact that the introduction arrives via connection allocated for the group, allows agents identify it as a new group member, ID used in introductions is group-scoped member ID.
|
||||
|
||||
note over A, BA: once all members were sent to B
|
||||
AA ->> BA: via gidAB: MEM C:midB
|
||||
|
||||
note over BA, B: once all members are connected
|
||||
BA ->> B: G:gidB MEM
|
||||
|
||||
note over A, AA: once all members reported connection
|
||||
AA ->> A: G:gidA MEM C:idAB
|
||||
|
||||
note over M, AA: for each member M
|
||||
|
||||
AA ->> MA: via gidAM: MEM C:midB
|
||||
MA ->> M: G:gidM MEM C:idMB
|
||||
|
||||
note over M, B: 3. B sends message to the group
|
||||
|
||||
B ->> BA: G:gidB SEND msg
|
||||
|
||||
note over BA: send message via associated broadcast and respond to client with SENT notifications
|
||||
|
||||
BA ->> B: G:gidB SENT C:idBA intMsgID
|
||||
BA ->> B: G:gidB SENT C:idBM intMsgID
|
||||
|
||||
note over BA, B: once sent to all
|
||||
BA ->> B: G:gidB SENT intMsgID
|
||||
|
||||
AA ->> A: G:gidA MSG C:idAB intMsgID msgdata
|
||||
A ->> AA: G:gidA ACK intMsgID
|
||||
AA ->> BA: via gidAB: RCVD extMsgID hash sig
|
||||
BA ->> B: G:gidA RCVD C:idBA intMsgID status<br>(status - message integrity check)
|
||||
|
||||
MA ->> M: G:gidM MSG C:idMB intMsgID msgdata
|
||||
M ->> MA: G:gidM ACK intMsgID
|
||||
MA ->> BA: via gidMB: RCVD extMsgID hash sig
|
||||
BA ->> B: G:gidM RCVD C:idBM intMsgID status
|
||||
|
||||
note over BA, B: once received by all
|
||||
BA ->> B: G:gidM RCVD intMsgID status
|
||||
|
||||
note over M, B: 4a. A leaves group
|
||||
|
||||
A ->> AA: G:gidA LEAVE
|
||||
AA ->> A: G:gidA OK
|
||||
AA ->> BA: via gidAB: LEFT
|
||||
note over AA: remove gidAB, remove from broadcast
|
||||
note over BA: remove gidBA, remove from broadcast
|
||||
BA ->> B: G:gidB LEFT C:idBA
|
||||
|
||||
AA ->> MA: via gidAM: LEFT
|
||||
note over AA: remove gidAM, remove from broadcast
|
||||
note over MA: remove gidMA, remove from broadcast
|
||||
MA ->> M: G:gidM LEFT C:idMA
|
||||
|
||||
AA ->> A: G:gidA LEFT
|
||||
|
||||
note over B, BA: if all members left
|
||||
BA ->> B: G:gidB: EMPTY
|
||||
|
||||
note over M, B: 4b. A removes B from group
|
||||
|
||||
A ->> AA: G:gidA REM C:idAB
|
||||
AA ->> A: G:gidA OK
|
||||
AA ->> BA: via gidAB: OUT
|
||||
note over BA: remove gidBA, all gidBM
|
||||
BA ->> B: G:gidB OUT C:idBA
|
||||
|
||||
note over AA: remove gidAB
|
||||
AA ->> A: G:gidA OK
|
||||
|
||||
note over M, B: below steps happen for each existing member M
|
||||
|
||||
AA ->> MA: via gidAM: REM C:midB
|
||||
note over MA: remove gidMB
|
||||
MA ->> AA: via gidMA: REMD C:midB
|
||||
MA ->> M: G:gidM REMD C:idMB C:idMA<br>(B removed by A)
|
||||
|
||||
note over A, AA: once all members removed B
|
||||
|
||||
AA ->> A: G:gidA REMD C:idAB<br>(B removed by this agent)
|
||||
|
||||
note over M, B: 4c. A deletes group
|
||||
A ->> AA: G:gidA DEL
|
||||
AA ->> A: G:gidA OK
|
||||
|
||||
AA ->> BA: via gidAB: DEL
|
||||
note over BA: remove all group connections and messages
|
||||
BA ->> B: G:gidB DELD C:idBA<br>(group deleted by A)
|
||||
BA ->> AA: via gidBA: DELD
|
||||
AA ->> A: G:gidA DELD C:idAB
|
||||
|
||||
AA ->> MA: via gidAM: DEL
|
||||
note over MA: remove all group connections and messages
|
||||
MA ->> M: G:gidM DELD C:idMA<br>(group deleted by A)
|
||||
MA ->> AA: via gidMA: DELD
|
||||
AA ->> A: G:gidA DELD C:idAM
|
||||
|
||||
AA ->> A: G:gidA DELD<br>(group deleted by this agent - all confirmed)
|
||||
@@ -0,0 +1,42 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice (A) - the introducer
|
||||
participant AA as Alice's<br>agent (AA)
|
||||
participant B as Bob (B) - introduced
|
||||
participant BA as Bob's<br>agent (BA)
|
||||
participant M as Mark (M) - introduced to
|
||||
participant MA as Mark's<br>agent (MA)
|
||||
|
||||
note over A, AA: 1. create introduction
|
||||
A ->> AA: C:idAB INTRO C:idAM infoM<br>(idAB - conn alias A has for B,<br>idAM - for M)
|
||||
AA ->> A: C:idAB OK
|
||||
|
||||
note over A, BA: 2. send intro to Bob
|
||||
|
||||
AA ->> BA: via idAB: INTRO C:extIntroIdM infoM
|
||||
BA ->> B: C:idBA REQ C:intIntroIdM infoM
|
||||
B ->> BA: C:idBM? ACPT C:intIntroIdM
|
||||
BA ->> B: C:idBM OK
|
||||
|
||||
note over BA: 3. create connection for M idBM
|
||||
|
||||
BA ->> AA: via idBA: INV C:extIntroIdM invBM infoB
|
||||
|
||||
note over AA, M: 4. send intro to Mark
|
||||
|
||||
AA ->> MA: via idAM: REQ C:extIntroIdB invBM infoB
|
||||
|
||||
note over MA, B: 5. Mark connects to Bob
|
||||
|
||||
MA ->> M: C:idMA REQ C:intIntroIdB infoB
|
||||
M ->> MA: C:idMB? ACPT C:intIntroIdB
|
||||
MA ->> M: C:idMB OK
|
||||
|
||||
MA ->> BA: establish connection idBM -> idMB
|
||||
|
||||
note over A, MA: 6. notify all clients
|
||||
|
||||
MA ->> M: C:idMB CON
|
||||
MA ->> AA: via idMA: CON C:extIntroIdB
|
||||
BA ->> B: C:idBM CON
|
||||
BA ->> AA: via idBA: CON C:extIntroIdB
|
||||
AA ->> A: C:idAB CON C:idAM
|
||||
@@ -0,0 +1,146 @@
|
||||
# Overview of SMP agent protocol commands
|
||||
|
||||
## Connections
|
||||
|
||||
### Commands and messages
|
||||
|
||||
A initiates connection, B accepts
|
||||
|
||||
- command `C:idB? NEW` - create connection
|
||||
- message `C:idB INV cInv`
|
||||
- command `C:idA? JOIN cInv replyMode` - join connection (response `OK`, followed by `CON`)
|
||||
- *message* `C:idB REQ prv:invId infoB` - request from B joining sent to A (not implemented)
|
||||
- *command* `C:idB ACPT prv:invId` - A confirms B joining (not implemented)
|
||||
- message `C:id CON` - connection is established
|
||||
- command `C:id SUB` - subscribe to connection
|
||||
- message `C:id END` - unsubscribed from connection
|
||||
- command `C:idB SEND msg` - send message
|
||||
- message `C:idA SENT msgId` - confirmation that the message is sent
|
||||
- message `C:id MSG msgId msgMeta msgIntegrity msgBody` - received message
|
||||
- *command* `C:idB ACK msgId` - acknowledge message reception (not implemented)
|
||||
- *message* `C:idA RCVD msgId msgIntegrity` - confirmation of message reception and integrity (not implemented)
|
||||
- command `C:id OFF` - suspend connection
|
||||
- command `C:id DEL` - delete connection
|
||||
- message `C:id? OK` - command confirmation
|
||||
- message `C:id? ERR e` - error
|
||||
|
||||
### Envelopes
|
||||
|
||||
- `MSG `
|
||||
- `HELLO verificationKey ackMode`
|
||||
- `REPLY replyInv`
|
||||
|
||||
## Broadcasts
|
||||
|
||||
### Commands & messages
|
||||
|
||||
- command `B:id? NEW` - create broadcast (response is `B:id OK`)
|
||||
- command `B:id SEND msg` - broadcast message (response is multiple `C:id SENT msgId` or ERR, separately for each connection, followed by `B:id SENT msgId` once sent to all)
|
||||
- message `B:id SENT msgId` - notification that the message is sent and its internal ID, same as SENT
|
||||
- command `B:id ADD cId` - add existing connection to a broadcast (response is `B:id OK` or `ERR`, e.g. if bId is used)
|
||||
- command `B:id REM cId` - remove connection from the broadcast (response is `REMD`)
|
||||
- message `B:id REMD cId` - connection removed from the broadcast
|
||||
- message `B:id EMPTY` - all connections were removed from the broadcast
|
||||
- command `B:id DEL` - delete broadcast (response is `B:id OK`)
|
||||
- command `B:id LS` - list connections in broadcast, response is `B:id MEM space_separated_connections`
|
||||
- message `B:id MEM space_separated_connections`
|
||||
|
||||
## Open/public connection
|
||||
|
||||
### Commands
|
||||
|
||||
- command `O:id? NEW` - create open connection
|
||||
- message `O:id INV oInv` - open invitation
|
||||
- command `C:id? JOIN oInv replyMode` - join connection (response `OK`, followed by `CON`)
|
||||
- message `O:id REQ open:invId infoB` - confirmation from B joining sent to A
|
||||
- command `C:idC? ACPT open:invId` - note, that it creates new connection, keeping OPEN connection
|
||||
- command `O:id SUB` - subscribe to open connection
|
||||
- message `O:id END` - unsubscribed from open connection
|
||||
- command `O:id OFF` - suspend open connection
|
||||
- command `O:id DEL` - delete open connection
|
||||
- message `O:id? OK` - command confirmation
|
||||
- message `O:id? ERR e` - error
|
||||
|
||||
## Introductions
|
||||
|
||||
### Commands
|
||||
|
||||
- command `C:idAB INTRO C:idAM infoM` - introduce connection cIdB to connection cIdM (response is `OK`)
|
||||
- message `C:idBA REQ C:invId infoM` - notification to confirm introduction
|
||||
- command `C:idBM? ACPT C:invId` - accept offer to be introduced (response is `cIdBM OK`, followed by `ICON`)
|
||||
- message `C:idBM CON` - confirmation that connection is established to both introduced parties
|
||||
- message `C:idAB CON C:idAM` - confirmation that connection is established to the introducer
|
||||
|
||||
### Envelopes
|
||||
|
||||
- `INTRO C:extIntroIdM infoM` - new introduction offered by introducer
|
||||
- `INV C:extIntroIdB prv:invBM infoB` - invitation to join connection from B to M sent via A (can be pub:)
|
||||
- `REQ C:extIntroIdB prv:invBM infoB` - new introduction forwarded by introducer
|
||||
- `CON C:extIntroIdM` - confirmation that the connection is established sent by both introduced parties to the introducer
|
||||
|
||||
## Groups
|
||||
|
||||
## Agent commands and messages syntax
|
||||
|
||||
- command `G:gId? NEW` - create group (response is `G:gId OK`)
|
||||
- command `C:cId INTRO G:gId gInfo` - add existing connection to a group
|
||||
- message `C:cId REQ g:invID gInfo` - invitation to join the group
|
||||
- command `G:gId? ACPT g:invId` - accept invitation (response is `G gId OK`)
|
||||
- message `G:gId CON C:cId` - 2 connections created with some group member (both for group and direct messages)
|
||||
- message `G:gId MEM [C:cId]` - connection created with all group members for a given member or current client
|
||||
- command `G:gId SEND msg` - send message to group
|
||||
- message `G:gId SENT msgId` - notification that the message is sent and its internal ID, same as SENT
|
||||
- message `G:gId MSG C:cId msgId msgdata` - received group message from cId, msgdata is the same set of parameters as in `MSG`
|
||||
- command `G:gId ACK msgId` - acknowledge message reception by the client
|
||||
- message `G:gId RCVD t:cId msgId status` - message delivery notification
|
||||
- command `G:gId LEAVE` - leave the group
|
||||
- message `G:gId LEFT [C:cId]` - connection cId left the group
|
||||
- command `G:gId REM C:cId` - remove group member (response is `gId OK`, followed by `GREMD` notification)
|
||||
- message `G:gId REMD C:cId [C:cId]` - member removed
|
||||
- message `G:gId OUT C:cId` - you are removed (see question below - should it be just a sequence of GLEFT?)
|
||||
- message `G:gId EMPTY` - all members left the group and it is now empty
|
||||
- command `G:gId DEL` - delete the group (response is `gId OK`)
|
||||
- message `G:gId DELD [C:cId]` - group deleted
|
||||
|
||||
## Agent message envelopes syntax
|
||||
|
||||
- `GROUP C:mid G:inv gInfo` - invitation to join the group
|
||||
- `MEM C:mid` - confirmation that member connected to all members
|
||||
- `LEFT` - notification that member left the group
|
||||
- `OUT` - you are removed from the group
|
||||
- `REM C:mid` - remove member mid from the group
|
||||
- `REMD C:mid` - confirmation that member is removed
|
||||
- `DEL` - group is deleted
|
||||
- `DELD` - confirmation that group is deleted
|
||||
|
||||
## Commands and objects
|
||||
|
||||
| Dir | Command / message | (C)onnection | (O)pen connection | (B)roadcast | (G)roup |
|
||||
|:---------:|:--------------------:|:------------:|:-----------------:|:-----------:|:-------:|
|
||||
| command | `t:id? NEW` | ✓ | ✓ | ✓ | ✓ |
|
||||
| command | `C:id INTRO t:id info` | ✓ | - | - | ✓ |
|
||||
| message | `t:id INV inv` | ✓ | ✓ | - | - |
|
||||
| command | `C:id? JOIN inv replyMode info` | ✓ | - | - | - |
|
||||
| message | `t:id REQ invId info` | ✓ | ✓ | - | ✓ |
|
||||
| command | `t:id? ACPT invId` | ✓ | - | - | ✓ |
|
||||
| message | `t:id CON [C:id]` | ✓ | - | - | ✓ |
|
||||
| message | `t:id MEM [C:id]` | - | - | - | ✓ |
|
||||
| command | `t:id SUB` | ✓ | ✓ | - | ✓ |
|
||||
| message | `t:id END` | ✓ | ✓ | - | ✓ |
|
||||
| command | `t:id OFF` | ✓ | ✓ | - | - |
|
||||
| command | `t:id DEL` | ✓ | ✓ | ✓ | ✓ |
|
||||
| message | `t:id DELD [C:Id]` | ✓ | ✓ | ✓ | ✓ |
|
||||
| command | `t:id SEND msg` | ✓ | - | ✓ | ✓ |
|
||||
| message | `t:id SENT [t':id] msgId` | ✓ | - | ✓ | ✓ |
|
||||
| message | `t:id MSG [C:id] msgId msgdata` | ✓ | - | - | ✓ |
|
||||
| command | `t:id ACK msgId` | ✓ | - | - | ✓ |
|
||||
| message | `t:id RCVD [t':id] msgId status` | ✓ | - | - | ✓ |
|
||||
| command | `t:id ADD C:id` | - | - | - | ✓ |
|
||||
| command | `t:id REM C:id` | - | - | ✓ | ✓ |
|
||||
| command | `t:id REMD C:id` | - | - | ✓ | ✓ |
|
||||
| message | `t:id EMPTY` | - | - | ✓ | ✓ |
|
||||
| message | `G:id OUT C:id` | - | - | - | ✓ |
|
||||
| command | `t:id LS` | - | - | ✓ | ✓ |
|
||||
| message | `t:id MS cIds` | - | - | ✓ | ✓ |
|
||||
| command | `G:id LEAVE` | - | - | - | ✓ |
|
||||
| message | `G:id LEFT [C:id]` | - | - | - | ✓ |
|
||||
@@ -0,0 +1,50 @@
|
||||
# SMP agent broadcast
|
||||
|
||||
## Problem
|
||||
|
||||
Support agent message broadcast to multiple connections.
|
||||
|
||||
It is done in ad-hoc way as part of the previous [groups proposal](./2021-05-23-groups2.md) - this proposal defines broadcast as a separate agent primitive to simplify group management.
|
||||
|
||||
It can also be used for other purposes when the same message needs to be sent to multiple recipients without creating groups.
|
||||
|
||||
## Solution
|
||||
|
||||
A minimal protocol of additional client commands to create, manage and use broadcasts.
|
||||
|
||||
From the point of view of the recipient this will look like a normal message, as if the sending agent executed multiple send commands (in fact, broadcast can be implemented by agent sending itself multiple SEND commands)
|
||||
|
||||
### Commands and messages
|
||||
|
||||
- command `B:bId? NEW` - create broadcast (response is `B:bId OK`, or `ERR` if broadcast already exists)
|
||||
- command `B:bId ADD C:cId` - add existing connection to a broadcast (response is `B:bId OK` or `ERR`, e.g. if connection already added or does not exist)
|
||||
- command `B:bId SEND msg` - broadcast message (response is multiple `B:cId SENT [C:bId] msgId` or ERR, separately for each connection and then for the broadcast)
|
||||
- message `B:bId SENT [C:bId] msgId` - notification that the message is sent to a specific or all recipients
|
||||
- command `B:bId REM C:cId` - remove connection from broadcast (response is `B:bId OK` or `ERR`)
|
||||
- message `B:bId EMPTY` - all connections were removed from the broadcast
|
||||
- command `B:bId DEL` - delete broadcast (response is `B:bId OK` and when the last connection is removed an additional `B:bId EMPTY` is sent)
|
||||
- command `B:bId LS` - list connections in broadcast, response is `B:Id MS space_separated_connections`
|
||||
- message `B:bId MS space_separated_connections`
|
||||
|
||||
## Questions
|
||||
|
||||
1. Should broadcast IDs use the same namespace as connection IDs (and as group IDs)? Having the same namespace for all abstractions that the agent can operate on can be helpful, as it can also allow implementing some queries to determine which type a given ID has, but it also increases implementation complexity.
|
||||
|
||||
2. Given that this abstraction would be used as internal abstraction for groups (same as connections internal to the group), it might be better to implement "agent users", each with its own connection namespace. In this case agent would use itself as one of the users.
|
||||
|
||||
3. There is a similarity of commands for connections, groups and broadcasts, they only differ on the single-letter prefix. We could do one of the following:
|
||||
- use the same command for different object types. This feels incorrect and error prone on its own.
|
||||
- extend transmission structure with the field defining the object type (connection, group, broadcast, etc.).
|
||||
|
||||
In this case, the transmission would look like:
|
||||
|
||||
```
|
||||
agentTransmission = [corrId] CRLF objectType:[objectID] CRLF agentCommand
|
||||
objectType = C | B | G ; this is the additional field
|
||||
```
|
||||
|
||||
This approach would allow reusing the existing command avoiding the unnecessary repetition.
|
||||
|
||||
In this case, the command type could be parameterized with the list of supported agent object types, so we can ensure on the type level that only allowed commands can be constructed.
|
||||
|
||||
EDIT: This approach is already implemented
|
||||
@@ -0,0 +1,41 @@
|
||||
# SMP agent introduction
|
||||
|
||||
## Problem
|
||||
|
||||
Allow an agent client to connect two connections it has directly, with the agent acting as an out-of-band channel.
|
||||
|
||||
It can be used both separately as part of some client functionality, and as part of group protocol.
|
||||
|
||||
## Solution
|
||||
|
||||
A protocol with commands and message envelopes to exchange the information between parties to establish connection.
|
||||
|
||||
### Commands and messages
|
||||
|
||||
Below commands are for the scenario when A introduces B to M.
|
||||
|
||||
- command `C:idAB INTRO C:idAM infoM` - initiate introduction of the connection cIdB to connection cIdM (response is `C:idAB OK`)
|
||||
- message `C:idBA REQ C:invId infoM` - notification to confirm introduction
|
||||
- command `C:idBM? ACPT C:invId` - accept offer to be introduced (response is `C:idBM OK`, followed by `C:idBM CON`)
|
||||
- message `C:idBM CON` - confirmation that connection is established to both introduced parties
|
||||
- message `C:idAB CON C:idAM` - confirmation that connection is established to the introducer
|
||||
|
||||
### Agent envelopes
|
||||
|
||||
- `INTRO C:extIntroIdM infoM` - new introduction offered by introducer
|
||||
- `INV C:extIntroIdB prv:invBM infoB` - invitation to join connection from B to M sent via A
|
||||
- `REQ C:extIntroIdB prv:invBM infoB` - new introduction forwarded by the introducer
|
||||
- `CON C:extIntroId` - confirmation that the connection is established sent by both introduced parties to the introducer
|
||||
|
||||
## Namespace
|
||||
|
||||
Given that the introduction objects are short lived, they should not reuse the same commands or share the same namespace as connections, broadcasts and groups, but they probably should share the namespace with group and connection invitations.
|
||||
|
||||
## Introduction protocol costs
|
||||
|
||||
5 messages + cost to establish a connection
|
||||
|
||||
|
||||
The [sequence digram for introduction](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEEgYXMgQWxpY2UgKEEpIC0gdGhlIGludHJvZHVjZXJcbiAgcGFydGljaXBhbnQgQUEgYXMgQWxpY2Unczxicj5hZ2VudCAoQUEpXG4gIHBhcnRpY2lwYW50IEIgYXMgQm9iIChCKSAtIGludHJvZHVjZWRcbiAgcGFydGljaXBhbnQgQkEgYXMgQm9iJ3M8YnI-YWdlbnQgKEJBKVxuICBwYXJ0aWNpcGFudCBNIGFzIE1hcmsgKE0pIC0gaW50cm9kdWNlZCB0b1xuICBwYXJ0aWNpcGFudCBNQSBhcyBNYXJrJ3M8YnI-YWdlbnQgKE1BKVxuXG4gIG5vdGUgb3ZlciBBLCBBQTogMS4gY3JlYXRlIGludHJvZHVjdGlvblxuICBBIC0-PiBBQTogQzppZEFCIElOVFJPIEM6aWRBTSBpbmZvTTxicj4oaWRBQiAtIGNvbm4gYWxpYXMgQSBoYXMgZm9yIEIsPGJyPmlkQU0gLSBmb3IgTSlcbiAgQUEgLT4-IEE6IEM6aWRBQiBPS1xuXG4gIG5vdGUgb3ZlciBBLCBCQTogMi4gc2VuZCBpbnRybyB0byBCb2JcblxuICBBQSAtPj4gQkE6IHZpYSBpZEFCOiBJTlRSTyBDOmV4dEludHJvSWRNIGluZm9NXG4gIEJBIC0-PiBCOiBDOmlkQkEgUkVRIEM6aW50SW50cm9JZE0gaW5mb01cbiAgQiAtPj4gQkE6IEM6aWRCTT8gQUNQVCBDOmludEludHJvSWRNXG4gIEJBIC0-PiBCOiBDOmlkQk0gT0tcblxuICBub3RlIG92ZXIgQkE6IDMuIGNyZWF0ZSBjb25uZWN0aW9uIGZvciAgTSBpZEJNXG5cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogSU5WIEM6ZXh0SW50cm9JZE0gcHJ2OmludkJNIGluZm9CXG5cbiAgbm90ZSBvdmVyIEFBLCBNOiA0LiBzZW5kIGludHJvIHRvIE1hcmtcblxuICBBQSAtPj4gTUE6IHZpYSBpZEFNOiBSRVEgQzpleHRJbnRyb0lkQiBwcnY6aW52Qk0gaW5mb0JcblxuICBub3RlIG92ZXIgTUEsIEI6IDUuIE1hcmsgY29ubmVjdHMgdG8gQm9iXG5cbiAgTUEgLT4-IE06IEM6aWRNQSBSRVEgQzppbnRJbnRyb0lkQiBpbmZvQlxuICBNIC0-PiBNQTogQzppZE1CPyBBQ1BUIEM6aW50SW50cm9JZEJcbiAgTUEgLT4-IE06IEM6aWRNQiBPS1xuXG4gIE1BIC0-PiBCQTogIGVzdGFibGlzaCBjb25uZWN0aW9uIGlkQk0gLT4gaWRNQlxuXG4gIG5vdGUgb3ZlciBBLCBNQTogNi4gbm90aWZ5IGFsbCBjbGllbnRzXG5cbiAgTUEgLT4-IE06IEM6aWRNQiBDT05cbiAgTUEgLT4-IEFBOiB2aWEgaWRNQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQkEgLT4-IEI6IEM6aWRCTSBDT05cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQUEgLT4-IEE6IEM6aWRBQiBDT04gQzppZEFNXG4iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0), the source is [here](./intro.mmd).
|
||||
|
||||

|
||||
@@ -0,0 +1,24 @@
|
||||
# Server image for DigitalOcean
|
||||
|
||||
The current image used for 1-click deployment on DigitalOcean does not contain the source or binary of SMP Server - it downloads the compiled binary of the latest release (rather than a particular release) from GitHub.
|
||||
|
||||
The upside is that the new image does not have to be created and approved by DigitalOcean every time when the new release is created.
|
||||
|
||||
The downside is that while the release is being prepared in CI, when the release object is already created in GitHub but the server binary is not attached yet, the attempt to install the server would fail - it can last anything from several to 20 minutes, depending on whether the cached dependencies were used or everything was recompiled. Currently, when there is a small number of users, it is not a big problem, but we should consider some possible solutions in the future. Linode StackScript has the same issue.
|
||||
|
||||
## How to build an image
|
||||
|
||||
1. [Create an API token](https://cloud.digitalocean.com/account/api/tokens) in vendor account in DigitalOcean.
|
||||
2. Install [packer](https://www.packer.io/downloads) downloading binary or with brew (on Mac):
|
||||
|
||||
```shell
|
||||
brew tap hashicorp/tap
|
||||
brew install hashicorp/tap/packer
|
||||
```
|
||||
|
||||
3. Run `packer build` in the `smp-server-digitalocean-droplet` repository:
|
||||
|
||||
```shell
|
||||
cd ./scripts/smp-server-digitalocean-droplet
|
||||
DIGITALOCEAN_TOKEN=$YOUR_TOKEN packer build -on-error=ask -color=false ./marketplace-image.json
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# receives pubkey_hash file location as the first parameter
|
||||
|
||||
ip_address=$(hostname -I | awk '{print$1}')
|
||||
hash=$(cat $1)
|
||||
|
||||
cat <<EOF
|
||||
********************************************************************************
|
||||
|
||||
SMP server address: $ip_address#$hash
|
||||
Check SMP server status with: systemctl status smp-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 80 (HTTP), 5223 (SMP server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
|
||||
EOF
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"variables": {
|
||||
"token": "{{env `DIGITALOCEAN_TOKEN`}}",
|
||||
"image_name": "smp-server-snapshot",
|
||||
"application_name": "SMP server"
|
||||
},
|
||||
"sensitive-variables": ["token"],
|
||||
"builders": [
|
||||
{
|
||||
"type": "digitalocean",
|
||||
"api_token": "{{user `token`}}",
|
||||
"image": "ubuntu-20-04-x64",
|
||||
"region": "fra1",
|
||||
"size": "s-1vcpu-1gb",
|
||||
"ssh_username": "root",
|
||||
"snapshot_name": "{{user `image_name`}}"
|
||||
}
|
||||
],
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": [
|
||||
"cloud-init status --wait",
|
||||
"mkdir -p /opt/simplex"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "files/opt/",
|
||||
"destination": "/opt/"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"environment_vars": [
|
||||
"application_name={{user `application_name`}}",
|
||||
"DEBIAN_FRONTEND=noninteractive"
|
||||
],
|
||||
"scripts": [
|
||||
"scripts/01-packages.sh",
|
||||
"scripts/02-firewall.sh",
|
||||
"scripts/03-init-start.sh",
|
||||
"scripts/90-cleanup.sh",
|
||||
"scripts/99-img-check.sh"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# update apt
|
||||
apt-get -y update
|
||||
apt-get -y upgrade
|
||||
|
||||
# packages needed for smp-server init-start.sh
|
||||
apt-get install -y jq
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# open ports
|
||||
ufw allow ssh
|
||||
ufw allow http
|
||||
ufw allow 5223
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
bin_dir="/opt/simplex/bin"
|
||||
conf_dir="/etc/opt/simplex"
|
||||
var_dir="/var/opt/simplex"
|
||||
mkdir -p $bin_dir
|
||||
mkdir -p $conf_dir
|
||||
mkdir -p $var_dir
|
||||
|
||||
echo "downloading the latest SMP server release"
|
||||
curl -s https://api.github.com/repos/simplex-chat/simplexmq/releases/latest > release.json
|
||||
jq '.assets[].browser_download_url | select(test("smp-server-ubuntu-20_04-x86-64"))' release.json \
|
||||
| tr -d \" \
|
||||
| wget -qi -
|
||||
|
||||
release_version=$(jq '.tag_name' release.json | tr -d \")
|
||||
echo "downloaded SMP server $release_version"
|
||||
rm release.json
|
||||
|
||||
echo "preparing for SMP server initiaization"
|
||||
mv smp-server-ubuntu-20_04-x86-64 $bin_dir/smp-server
|
||||
chmod +x $bin_dir/smp-server
|
||||
|
||||
cat <<EOT >> /etc/profile.d/simplex.sh
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOT
|
||||
source /etc/profile.d/simplex.sh
|
||||
|
||||
# prepare SMP server systemd service
|
||||
cat <<EOT >> /etc/systemd/system/smp-server.service
|
||||
[Unit]
|
||||
Description=SMP server systemd service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "$bin_dir/smp-server start"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOT
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
|
||||
echo "initializing SMP server"
|
||||
hash_file="$conf_dir/pubkey_hash"
|
||||
smp-server init -l | grep "transport key hash:" | cut -f2 -d":" | xargs > $hash_file
|
||||
# turn off websockets support
|
||||
sed -e '/websockets/s/^/# /g' -i $conf_dir/smp-server.ini
|
||||
# add welcome script to .bashrc
|
||||
echo "bash /opt/simplex/on_login.sh $hash_file" >> /root/.bashrc
|
||||
|
||||
echo "starting SMP server"
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DigitalOcean Marketplace Image Validation Tool
|
||||
# © 2021 DigitalOcean LLC.
|
||||
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
|
||||
|
||||
set -o errexit
|
||||
|
||||
# Ensure /tmp exists and has the proper permissions before
|
||||
# checking for security updates
|
||||
# https://github.com/digitalocean/marketplace-partners/issues/94
|
||||
if [[ ! -d /tmp ]]; then
|
||||
mkdir /tmp
|
||||
fi
|
||||
chmod 1777 /tmp
|
||||
|
||||
if [ -n "$(command -v yum)" ]; then
|
||||
yum update -y
|
||||
yum clean all
|
||||
elif [ -n "$(command -v apt-get)" ]; then
|
||||
apt-get -y update
|
||||
apt-get -y upgrade
|
||||
apt-get -y autoremove
|
||||
apt-get -y autoclean
|
||||
fi
|
||||
|
||||
rm -rf /tmp/* /var/tmp/*
|
||||
history -c
|
||||
cat /dev/null > /root/.bash_history
|
||||
unset HISTFILE
|
||||
find /var/log -mtime -1 -type f -exec truncate -s 0 {} \;
|
||||
rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????
|
||||
rm -rf /var/lib/cloud/instances/*
|
||||
rm -f /root/.ssh/authorized_keys /etc/ssh/*key*
|
||||
touch /etc/ssh/revoked_keys
|
||||
chmod 600 /etc/ssh/revoked_keys
|
||||
|
||||
# Securely erase the unused portion of the filesystem
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
printf "\n${GREEN}Writing zeros to the remaining disk space to securely
|
||||
erase the unused portion of the file system.
|
||||
Depending on your disk size this may take several minutes.
|
||||
The secure erase will complete successfully when you see:${NC}
|
||||
dd: writing to '/zerofile': No space left on device\n
|
||||
Beginning secure erase now\n"
|
||||
|
||||
dd if=/dev/zero of=/zerofile &
|
||||
PID=$!
|
||||
while [ -d /proc/$PID ]
|
||||
do
|
||||
printf "."
|
||||
sleep 5
|
||||
done
|
||||
sync; rm /zerofile; sync
|
||||
cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp
|
||||
|
||||
rm /var/log/auth.log
|
||||
rm /var/log/kern.log
|
||||
rm /var/log/ufw.log
|
||||
@@ -0,0 +1,682 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DigitalOcean Marketplace Image Validation Tool
|
||||
# © 2021 DigitalOcean LLC.
|
||||
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
|
||||
|
||||
VERSION="v. 1.6"
|
||||
RUNDATE=$( date )
|
||||
|
||||
# Script should be run with SUDO
|
||||
if [ "$EUID" -ne 0 ]
|
||||
then echo "[Error] - This script must be run with sudo or as the root user."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATUS=0
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
|
||||
# $1 == command to check for
|
||||
# returns: 0 == true, 1 == false
|
||||
cmdExists() {
|
||||
if command -v "$1" > /dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function getDistro {
|
||||
if [ -f /etc/os-release ]; then
|
||||
# freedesktop.org and systemd
|
||||
. /etc/os-release
|
||||
OS=$NAME
|
||||
VER=$VERSION_ID
|
||||
elif type lsb_release >/dev/null 2>&1; then
|
||||
# linuxbase.org
|
||||
OS=$(lsb_release -si)
|
||||
VER=$(lsb_release -sr)
|
||||
elif [ -f /etc/lsb-release ]; then
|
||||
# For some versions of Debian/Ubuntu without lsb_release command
|
||||
. /etc/lsb-release
|
||||
OS=$DISTRIB_ID
|
||||
VER=$DISTRIB_RELEASE
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
# Older Debian/Ubuntu/etc.
|
||||
OS=Debian
|
||||
VER=$(cat /etc/debian_version)
|
||||
elif [ -f /etc/SuSe-release ]; then
|
||||
# Older SuSE/etc.
|
||||
:
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
# Older Red Hat, CentOS, etc.
|
||||
VER=$( cat /etc/redhat-release | cut -d" " -f3 | cut -d "." -f1)
|
||||
d=$( cat /etc/redhat-release | cut -d" " -f1 | cut -d "." -f1)
|
||||
if [[ $d == "CentOS" ]]; then
|
||||
OS="CentOS Linux"
|
||||
fi
|
||||
else
|
||||
# Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
|
||||
OS=$(uname -s)
|
||||
VER=$(uname -r)
|
||||
fi
|
||||
}
|
||||
function loadPasswords {
|
||||
SHADOW=$(cat /etc/shadow)
|
||||
}
|
||||
|
||||
function checkAgent {
|
||||
# Check for the presence of the do-agent in the filesystem
|
||||
if [ -d /var/opt/digitalocean/do-agent ];then
|
||||
echo -en "\e[41m[FAIL]\e[0m DigitalOcean Monitoring Agent detected.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
if [[ $OS == "CentOS Linux" ]]; then
|
||||
echo "The agent can be removed with 'sudo yum remove do-agent' "
|
||||
elif [[ $OS == "Ubuntu" ]]; then
|
||||
echo "The agent can be removed with 'sudo apt-get purge do-agent' "
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n"
|
||||
((PASS++))
|
||||
fi
|
||||
}
|
||||
|
||||
function checkLogs {
|
||||
cp_ignore="/var/log/cpanel-install.log"
|
||||
echo -en "\nChecking for log files in /var/log\n\n"
|
||||
# Check if there are log archives or log files that have not been recently cleared.
|
||||
for f in /var/log/*-????????; do
|
||||
[[ -e $f ]] || break
|
||||
if [ $f != $cp_ignore ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
for f in /var/log/*.[0-9];do
|
||||
[[ -e $f ]] || break
|
||||
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
done
|
||||
for f in /var/log/*.log; do
|
||||
[[ -e $f ]] || break
|
||||
if [[ "${f}" = '/var/log/lfd.log' && "$( cat "${f}" | egrep -v '/var/log/messages has been reset| Watching /var/log/messages' | wc -c)" -gt 50 ]]; then
|
||||
if [ $f != $cp_ignore ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
elif [[ "${f}" != '/var/log/lfd.log' && "$( cat "${f}" | wc -c)" -gt 50 ]]; then
|
||||
if [ $f != $cp_ignore ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
function checkTMP {
|
||||
# Check the /tmp directory to ensure it is empty. Warn on any files found.
|
||||
return 1
|
||||
}
|
||||
function checkRoot {
|
||||
user="root"
|
||||
uhome="/root"
|
||||
for usr in $SHADOW
|
||||
do
|
||||
IFS=':' read -r -a u <<< "$usr"
|
||||
if [[ "${u[0]}" == "${user}" ]]; then
|
||||
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ -d ${uhome}/ ]; then
|
||||
if [ -d ${uhome}/.ssh/ ]; then
|
||||
if ls ${uhome}/.ssh/*> /dev/null 2>&1; then
|
||||
for key in ${uhome}/.ssh/*
|
||||
do
|
||||
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
|
||||
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
else
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
|
||||
fi
|
||||
if [ -f /root/.bash_history ];then
|
||||
|
||||
BH_S=$( cat /root/.bash_history | wc -c)
|
||||
|
||||
if [[ $BH_S -lt 200 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
|
||||
return 1;
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
|
||||
fi
|
||||
echo -en "\n\n"
|
||||
return 1
|
||||
}
|
||||
|
||||
function checkUsers {
|
||||
# Check each user-created account
|
||||
for user in $(awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' /etc/passwd;)
|
||||
do
|
||||
# Skip some other non-user system accounts
|
||||
if [[ $user == "centos" ]]; then
|
||||
:
|
||||
elif [[ $user == "nfsnobody" ]]; then
|
||||
:
|
||||
else
|
||||
echo -en "\nChecking user: ${user}...\n"
|
||||
for usr in $SHADOW
|
||||
do
|
||||
IFS=':' read -r -a u <<< "$usr"
|
||||
if [[ "${u[0]}" == "${user}" ]]; then
|
||||
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
#echo "User Found: ${user}"
|
||||
uhome="/home/${user}"
|
||||
if [ -d "${uhome}/" ]; then
|
||||
if [ -d "${uhome}/.ssh/" ]; then
|
||||
if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then
|
||||
for key in ${uhome}/.ssh/*
|
||||
do
|
||||
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
|
||||
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
done
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
|
||||
fi
|
||||
|
||||
# Check for an uncleared .bash_history for this user
|
||||
if [ -f "${uhome}/.bash_history" ]; then
|
||||
BH_S=$( cat "${uhome}/.bash_history" | wc -c )
|
||||
|
||||
if [[ $BH_S -lt 200 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
|
||||
fi
|
||||
echo -en "\n\n"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
function checkFirewall {
|
||||
|
||||
if [[ $OS == "Ubuntu" ]]; then
|
||||
fw="ufw"
|
||||
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
|
||||
if [[ $ufwa == "active" ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
if [ -f /usr/lib/systemd/system/csf.service ]; then
|
||||
fw="csf"
|
||||
if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then
|
||||
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
elif cmdExists "firewall-cmd"; then
|
||||
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
else
|
||||
fw="firewalld"
|
||||
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
fi
|
||||
elif [[ "$OS" =~ Debian.* ]]; then
|
||||
# user could be using a number of different services for managing their firewall
|
||||
# we will check some of the most common
|
||||
if cmdExists 'ufw'; then
|
||||
fw="ufw"
|
||||
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
|
||||
if [[ $ufwa == "active" ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
elif cmdExists "firewall-cmd"; then
|
||||
fw="firewalld"
|
||||
if [[ $(systemctl is-active --quiet $fw) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
else
|
||||
# user could be using vanilla iptables, check if kernel module is loaded
|
||||
fw="iptables"
|
||||
if [[ $(lsmod | grep -q '^ip_tables' 2>/dev/null) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
}
|
||||
function checkUpdates {
|
||||
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
|
||||
# Ensure /tmp exists and has the proper permissions before
|
||||
# checking for security updates
|
||||
# https://github.com/digitalocean/marketplace-partners/issues/94
|
||||
if [[ ! -d /tmp ]]; then
|
||||
mkdir /tmp
|
||||
fi
|
||||
chmod 1777 /tmp
|
||||
|
||||
echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n"
|
||||
apt-get -y update > /dev/null
|
||||
|
||||
uc=$(apt-get --just-print upgrade | grep -i "security" | wc -l)
|
||||
if [[ $uc -gt 0 ]]; then
|
||||
update_count=$(( ${uc} / 2 ))
|
||||
else
|
||||
update_count=0
|
||||
fi
|
||||
|
||||
if [[ $update_count -gt 0 ]]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
|
||||
echo -en
|
||||
echo -en "Here is a list of the security updates that are not installed:\n"
|
||||
sleep 2
|
||||
apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'
|
||||
echo -en
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
|
||||
fi
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
echo -en "\nChecking for available security updates, this may take a minute...\n\n"
|
||||
|
||||
update_count=$(yum check-update --security --quiet | wc -l)
|
||||
if [[ $update_count -gt 0 ]]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
echo "Error encountered"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
return 1;
|
||||
}
|
||||
function checkCloudInit {
|
||||
|
||||
if hash cloud-init 2>/dev/null; then
|
||||
CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n"
|
||||
((PASS++))
|
||||
else
|
||||
CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
function checkMongoDB {
|
||||
# Check if MongoDB is installed
|
||||
# If it is, verify the version is allowed (non-SSPL)
|
||||
|
||||
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
|
||||
|
||||
if [[ -f "/usr/bin/mongod" ]]; then
|
||||
version=$(/usr/bin/mongod --version --quiet | grep "db version" | sed -e "s/^db\ version\ v//")
|
||||
|
||||
if version_gt $version 4.0.0; then
|
||||
if version_gt $version 4.0.3; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present, ${version}"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed, ${version} is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
if version_gt $version 3.6.8; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present, ${version}"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed, ${version} is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m MongoDB is not installed"
|
||||
((PASS++))
|
||||
fi
|
||||
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
|
||||
if [[ -f "/usr/bin/mongod" ]]; then
|
||||
version=$(/usr/bin/mongod --version --quiet | grep "db version" | sed -e "s/^db\ version\ v//")
|
||||
|
||||
|
||||
if version_gt $version 4.0.0; then
|
||||
if version_gt $version 4.0.3; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
if version_gt $version 3.6.8; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m MongoDB is not installed"
|
||||
((PASS++))
|
||||
fi
|
||||
|
||||
else
|
||||
echo "ERROR: Unable to identify distribution"
|
||||
((FAIL++))
|
||||
STATUS 2
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
}
|
||||
|
||||
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
|
||||
|
||||
|
||||
clear
|
||||
echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}"
|
||||
echo "Executed on: ${RUNDATE}"
|
||||
echo "Checking local system for Marketplace compatibility..."
|
||||
|
||||
getDistro
|
||||
|
||||
echo -en "\n\e[1mDistribution:\e[0m ${OS}\n"
|
||||
echo -en "\e[1mVersion:\e[0m ${VER}\n\n"
|
||||
|
||||
ost=0
|
||||
osv=0
|
||||
|
||||
if [[ $OS == "Ubuntu" ]]; then
|
||||
ost=1
|
||||
if [[ $VER == "20.04" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "18.04" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "16.04" ]]; then
|
||||
osv=1
|
||||
else
|
||||
osv=0
|
||||
fi
|
||||
|
||||
elif [[ "$OS" =~ Debian.* ]]; then
|
||||
ost=1
|
||||
case "$VER" in
|
||||
9)
|
||||
osv=1
|
||||
;;
|
||||
10)
|
||||
osv=1
|
||||
;;
|
||||
*)
|
||||
osv=2
|
||||
;;
|
||||
esac
|
||||
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
ost=1
|
||||
if [[ $VER == "8" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "7" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "6" ]]; then
|
||||
osv=1
|
||||
else
|
||||
osv=2
|
||||
fi
|
||||
else
|
||||
ost=0
|
||||
fi
|
||||
|
||||
if [[ $ost == 1 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
|
||||
if [[ $osv == 1 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n"
|
||||
((PASS++))
|
||||
elif [[ $ost == 1 ]]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo "Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkCloudInit
|
||||
|
||||
echo -en "${CI}"
|
||||
|
||||
checkFirewall
|
||||
|
||||
echo -en "${FW_VER}"
|
||||
|
||||
checkUpdates
|
||||
|
||||
loadPasswords
|
||||
|
||||
checkLogs
|
||||
|
||||
echo -en "\n\nChecking all user-created accounts...\n"
|
||||
checkUsers
|
||||
|
||||
echo -en "\n\nChecking the root account...\n"
|
||||
checkRoot
|
||||
|
||||
checkAgent
|
||||
|
||||
checkMongoDB
|
||||
|
||||
|
||||
# Summary
|
||||
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"
|
||||
|
||||
if [[ $STATUS == 0 ]]; then
|
||||
echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n"
|
||||
elif [[ $STATUS == 1 ]]; then
|
||||
echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n"
|
||||
else
|
||||
echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n"
|
||||
fi
|
||||
echo "---------------------------------------------------------------------------------------------------"
|
||||
echo -en "\e[1m${PASS} Tests PASSED\e[0m\n"
|
||||
echo -en "\e[1m${WARN} WARNINGS\e[0m\n"
|
||||
echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n"
|
||||
echo -en "---------------------------------------------------------------------------------------------------\n"
|
||||
|
||||
if [[ $STATUS == 0 ]]; then
|
||||
echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n"
|
||||
exit 0
|
||||
elif [[ $STATUS == 1 ]]; then
|
||||
echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n"
|
||||
exit 0
|
||||
else
|
||||
echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
# <UDF name="enable_store_log" label="Store log - persists SMP queues to append only log and restores them upon server restart." default="on" oneof="on, off" />
|
||||
# <UDF name="api_token" label="Linode API token - enables StackScript to create tags containing SMP server domain/ip address, transport key hash and server version. Use `domain#hash` or `ip#hash` as SMP server address in the client. Note: minimal permissions token should have are - read/write access to `linodes` (to update linode tags - you need them) and `domains` (to add A record for the chosen 3rd level domain)" default="" />
|
||||
# <UDF name="fqdn" label="FQDN (Fully qualified domain name) - provide third level domain name (ex: smp.example.com). If provided can be used instead of ip address." default="" />
|
||||
|
||||
# log all stdout output to stackscript.log
|
||||
exec &> >(tee -i /var/log/stackscript.log)
|
||||
# uncomment next line to enable debugging features
|
||||
# set -xeo pipefail
|
||||
|
||||
cd $HOME
|
||||
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y upgrade
|
||||
sudo apt-get install -y jq
|
||||
|
||||
# add firewall
|
||||
echo "y" | ufw enable
|
||||
# open ports
|
||||
ufw allow ssh
|
||||
ufw allow http
|
||||
ufw allow 5223
|
||||
|
||||
bin_dir="/opt/simplex/bin"
|
||||
conf_dir="/etc/opt/simplex"
|
||||
var_dir="/var/opt/simplex"
|
||||
mkdir -p $bin_dir
|
||||
mkdir -p $conf_dir
|
||||
mkdir -p $var_dir
|
||||
|
||||
# retrieve latest release info and download smp-server executable
|
||||
curl -s https://api.github.com/repos/simplex-chat/simplexmq/releases/latest > release.json
|
||||
jq '.assets[].browser_download_url | select(test("smp-server-ubuntu-20_04-x86-64"))' release.json \
|
||||
| tr -d \" \
|
||||
| wget -qi -
|
||||
|
||||
mv smp-server-ubuntu-20_04-x86-64 $bin_dir/smp-server
|
||||
chmod +x $bin_dir/smp-server
|
||||
|
||||
cat <<EOT >> /etc/profile.d/simplex.sh
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOT
|
||||
source /etc/profile.d/simplex.sh
|
||||
|
||||
# initialize SMP server
|
||||
init_opts=()
|
||||
[[ $ENABLE_STORE_LOG == "on" ]] && init_opts+=(-l)
|
||||
hash_file="$conf_dir/pubkey_hash"
|
||||
smp-server init "${init_opts[@]}" | grep "transport key hash:" | cut -f2 -d":" | xargs > $hash_file
|
||||
# turn off websockets support
|
||||
sed -e '/websockets/s/^/# /g' -i $conf_dir/smp-server.ini
|
||||
# create script that will run on login
|
||||
on_login_script="/opt/simplex/on_login.sh"
|
||||
cat <<EOT >> $on_login_script
|
||||
#!/bin/bash
|
||||
# receives pubkey_hash file location as the first parameter
|
||||
|
||||
ip_address=\$(hostname -I | awk '{print\$1}')
|
||||
hash=\$(cat \$1)
|
||||
|
||||
cat <<EOF
|
||||
********************************************************************************
|
||||
|
||||
SMP server address: \$ip_address#\$hash
|
||||
Check SMP server status with: systemctl status smp-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 80 (HTTP), 5223 (SMP server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
|
||||
EOF
|
||||
|
||||
EOT
|
||||
chmod +x $on_login_script
|
||||
echo "bash $on_login_script $hash_file" >> /root/.bashrc
|
||||
|
||||
# create A record and update linode's tags
|
||||
if [ ! -z "$API_TOKEN" ]; then
|
||||
ip_address=$(curl ifconfig.me)
|
||||
address=$ip_address
|
||||
if [ ! -z "$FQDN" ]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ ! -z $domain_id ]]; then
|
||||
curl -s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
address=$FQDN
|
||||
fi
|
||||
fi
|
||||
|
||||
hash=$(cat $hash_file)
|
||||
release_version=$(jq '.tag_name' release.json | tr -d \")
|
||||
|
||||
curl -s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$address\",\"#$hash\",\"$release_version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
fi
|
||||
|
||||
# create, enable and start SMP server systemd service
|
||||
cat <<EOT >> /etc/systemd/system/smp-server.service
|
||||
[Unit]
|
||||
Description=SMP server systemd service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "$bin_dir/smp-server start"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOT
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
|
||||
# cleanup
|
||||
rm release.json
|
||||
@@ -0,0 +1,260 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.34.4.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
--
|
||||
-- hash: f5e649e0c359792d9528dec6e6cf07af772a521d746e66b8cc2b86d8c604afb2
|
||||
|
||||
name: simplexmq
|
||||
version: 0.5.2
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
|
||||
.
|
||||
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md SMP protocol>
|
||||
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md SMP agent protocol>
|
||||
.
|
||||
See <https://github.com/simplex-chat/simplex-chat terminal chat prototype> built with SimpleXMQ broker.
|
||||
category: Chat, Network, Web, System, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020 simplex.chat
|
||||
license: AGPL-3
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
migrations/20210101_initial.sql
|
||||
migrations/20210624_confirmations.sql
|
||||
migrations/20210809_snd_messages.sql
|
||||
migrations/20211202_connection_mode.sql
|
||||
migrations/README.md
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.Messaging.Agent
|
||||
Simplex.Messaging.Agent.Client
|
||||
Simplex.Messaging.Agent.Env.SQLite
|
||||
Simplex.Messaging.Agent.Protocol
|
||||
Simplex.Messaging.Agent.RetryInterval
|
||||
Simplex.Messaging.Agent.Store
|
||||
Simplex.Messaging.Agent.Store.SQLite
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Crypto
|
||||
Simplex.Messaging.Parsers
|
||||
Simplex.Messaging.Protocol
|
||||
Simplex.Messaging.Server
|
||||
Simplex.Messaging.Server.Env.STM
|
||||
Simplex.Messaging.Server.MsgStore
|
||||
Simplex.Messaging.Server.MsgStore.STM
|
||||
Simplex.Messaging.Server.QueueStore
|
||||
Simplex.Messaging.Server.QueueStore.STM
|
||||
Simplex.Messaging.Server.StoreLog
|
||||
Simplex.Messaging.Transport
|
||||
Simplex.Messaging.Transport.WebSockets
|
||||
Simplex.Messaging.Util
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
src
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, 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.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlite ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable smp-agent
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/smp-agent
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, 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.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlite ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, sqlite-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable smp-server
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/smp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, 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.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, direct-sqlite ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, ini ==0.4.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, sqlite-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite smp-server-test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Test.hs
|
||||
other-modules:
|
||||
AgentTests
|
||||
AgentTests.ConnectionRequestTests
|
||||
AgentTests.FunctionalAPITests
|
||||
AgentTests.SQLiteTests
|
||||
ProtocolErrorTests
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
SMPClient
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
tests
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
build-depends:
|
||||
HUnit ==1.6.*
|
||||
, QuickCheck ==2.14.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, 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.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlite ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, hspec ==2.7.*
|
||||
, hspec-core ==2.7.*
|
||||
, http-types ==0.12.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, sqlite-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, timeit ==2.0.*
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
default-language: Haskell2010
|
||||
@@ -1,65 +1,185 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Agent
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- This module defines SMP protocol agent with SQLite persistence.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md
|
||||
module Simplex.Messaging.Agent
|
||||
( runSMPAgent,
|
||||
( -- * SMP agent over TCP
|
||||
runSMPAgent,
|
||||
runSMPAgentBlocking,
|
||||
|
||||
-- * queue-based SMP agent
|
||||
getAgentClient,
|
||||
runAgentClient,
|
||||
|
||||
-- * SMP agent functional API
|
||||
AgentClient (..),
|
||||
AgentMonad,
|
||||
AgentErrorMonad,
|
||||
getSMPAgentClient,
|
||||
runSMPAgentClient,
|
||||
disconnectAgentClient, -- used in tests
|
||||
withAgentLock,
|
||||
createConnection,
|
||||
joinConnection,
|
||||
allowConnection,
|
||||
acceptContact,
|
||||
rejectContact,
|
||||
subscribeConnection,
|
||||
sendMessage,
|
||||
ackMessage,
|
||||
suspendConnection,
|
||||
deleteConnection,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
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.Bifunctor (second)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Composition ((.:), (.:.))
|
||||
import Data.Functor (($>))
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (isJust)
|
||||
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.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, connectSQLiteStore)
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
|
||||
import Simplex.Messaging.Client (SMPServerTransmission)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (CorrId (..), MsgBody, SenderPublicKey)
|
||||
import Simplex.Messaging.Protocol (MsgBody, SenderPublicKey)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport (putLn, runTCPServer)
|
||||
import Simplex.Messaging.Util (liftError)
|
||||
import System.IO (Handle)
|
||||
import UnliftIO.Async (race_)
|
||||
import UnliftIO.Exception (SomeException)
|
||||
import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), currentSMPVersionStr, runTransportServer)
|
||||
import Simplex.Messaging.Util (bshow, tryError, unlessM)
|
||||
import System.Random (randomR)
|
||||
import UnliftIO.Async (async, race_)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m ()
|
||||
runSMPAgent cfg@AgentConfig {tcpPort} = runReaderT smpAgent =<< newSMPAgentEnv cfg
|
||||
-- | Runs an SMP agent as a TCP service using passed configuration.
|
||||
--
|
||||
-- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs
|
||||
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m ()
|
||||
runSMPAgent t cfg = do
|
||||
started <- newEmptyTMVarIO
|
||||
runSMPAgentBlocking t started cfg
|
||||
|
||||
-- | Runs an SMP agent as a TCP service using passed configuration with signalling.
|
||||
--
|
||||
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
|
||||
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
|
||||
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m ()
|
||||
runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReaderT (smpAgent t) =<< newSMPAgentEnv cfg
|
||||
where
|
||||
smpAgent :: (MonadUnliftIO m', MonadReader Env m') => m' ()
|
||||
smpAgent = runTCPServer tcpPort $ \h -> do
|
||||
liftIO $ putLn h "Welcome to SMP v0.2.0 agent"
|
||||
c <- getSMPAgentClient
|
||||
smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' ()
|
||||
smpAgent _ = runTransportServer started tcpPort $ \(h :: c) -> do
|
||||
liftIO . putLn h $ "Welcome to SMP agent v" <> currentSMPVersionStr
|
||||
c <- getAgentClient
|
||||
logConnection c True
|
||||
race_ (connectClient h c) (runSMPAgentClient c)
|
||||
`E.finally` (closeSMPServerClients c >> logConnection c False)
|
||||
race_ (connectClient h c) (runAgentClient c)
|
||||
`E.finally` disconnectAgentClient c
|
||||
|
||||
getSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient
|
||||
getSMPAgentClient = do
|
||||
n <- asks clientCounter
|
||||
cfg <- asks config
|
||||
atomically $ newAgentClient n cfg
|
||||
-- | Creates an SMP agent client instance
|
||||
getSMPAgentClient :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m AgentClient
|
||||
getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent
|
||||
where
|
||||
runAgent = do
|
||||
c <- getAgentClient
|
||||
action <- async $ subscriber c `E.finally` disconnectAgentClient c
|
||||
pure c {smpSubscriber = action}
|
||||
|
||||
connectClient :: MonadUnliftIO m => Handle -> AgentClient -> m ()
|
||||
disconnectAgentClient :: MonadUnliftIO m => AgentClient -> m ()
|
||||
disconnectAgentClient c = closeAgentClient c >> logConnection c False
|
||||
|
||||
-- |
|
||||
type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m)
|
||||
|
||||
-- | Create SMP agent connection (NEW command)
|
||||
createConnection :: AgentErrorMonad m => AgentClient -> SConnectionMode c -> m (ConnId, ConnectionRequest c)
|
||||
createConnection c cMode = withAgentEnv c $ newConn c "" cMode
|
||||
|
||||
-- | Join SMP agent connection (JOIN command)
|
||||
joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest c -> ConnInfo -> m ConnId
|
||||
joinConnection c = withAgentEnv c .: joinConn c ""
|
||||
|
||||
-- | Allow connection to continue after CONF notification (LET command)
|
||||
allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m ()
|
||||
allowConnection c = withAgentEnv c .:. allowConnection' c
|
||||
|
||||
-- | Accept contact after REQ notification (ACPT command)
|
||||
acceptContact :: AgentErrorMonad m => AgentClient -> ConfirmationId -> ConnInfo -> m ConnId
|
||||
acceptContact c = withAgentEnv c .: acceptContact' c ""
|
||||
|
||||
-- | Reject contact (RJCT command)
|
||||
rejectContact :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> m ()
|
||||
rejectContact c = withAgentEnv c .: rejectContact' c
|
||||
|
||||
-- | Subscribe to receive connection messages (SUB command)
|
||||
subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
|
||||
subscribeConnection c = withAgentEnv c . subscribeConnection' c
|
||||
|
||||
-- | Send message to the connection (SEND command)
|
||||
sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId
|
||||
sendMessage c = withAgentEnv c .: sendMessage' c
|
||||
|
||||
ackMessage :: AgentErrorMonad m => AgentClient -> ConnId -> AgentMsgId -> m ()
|
||||
ackMessage c = withAgentEnv c .: ackMessage' c
|
||||
|
||||
-- | Suspend SMP agent connection (OFF command)
|
||||
suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
|
||||
suspendConnection c = withAgentEnv c . suspendConnection' c
|
||||
|
||||
-- | Delete SMP agent connection (DEL command)
|
||||
deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
|
||||
deleteConnection c = withAgentEnv c . deleteConnection' c
|
||||
|
||||
withAgentEnv :: AgentClient -> ReaderT Env m a -> m a
|
||||
withAgentEnv c = (`runReaderT` agentEnv c)
|
||||
|
||||
-- withAgentClient :: AgentErrorMonad m => AgentClient -> ReaderT Env m a -> m a
|
||||
-- withAgentClient c = withAgentLock c . withAgentEnv c
|
||||
|
||||
-- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's.
|
||||
getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient
|
||||
getAgentClient = ask >>= atomically . newAgentClient
|
||||
|
||||
connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m ()
|
||||
connectClient h c = race_ (send h c) (receive h c)
|
||||
|
||||
logConnection :: MonadUnliftIO m => AgentClient -> Bool -> m ()
|
||||
@@ -67,235 +187,485 @@ 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)
|
||||
-- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's.
|
||||
runAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
|
||||
runAgentClient c = race_ (subscriber c) (client c)
|
||||
|
||||
receive :: forall m. MonadUnliftIO m => Handle -> AgentClient -> m ()
|
||||
receive h c@AgentClient {rcvQ, sndQ} = forever $ do
|
||||
(corrId, cAlias, cmdOrErr) <- tGet SClient h
|
||||
receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
|
||||
receive h c@AgentClient {rcvQ, subQ} = forever $ do
|
||||
(corrId, connId, cmdOrErr) <- tGet SClient h
|
||||
case cmdOrErr of
|
||||
Right cmd -> write rcvQ (corrId, cAlias, cmd)
|
||||
Left e -> write sndQ (corrId, cAlias, ERR e)
|
||||
Right cmd -> write rcvQ (corrId, connId, cmd)
|
||||
Left e -> write subQ (corrId, connId, 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
|
||||
send :: (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
|
||||
send h c@AgentClient {subQ} = forever $ do
|
||||
t <- atomically $ readTBQueue subQ
|
||||
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 [B.pack $ show clientId, dir, "A :", corrId, cAlias, B.takeWhile (/= ' ') $ serializeCommand cmd]
|
||||
logClient AgentClient {clientId} dir (corrId, connId, cmd) = do
|
||||
logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, 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 ()
|
||||
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
|
||||
client c@AgentClient {rcvQ, subQ} = forever $ do
|
||||
(corrId, connId, cmd) <- atomically $ readTBQueue rcvQ
|
||||
withAgentLock c (runExceptT $ processCommand c (connId, cmd))
|
||||
>>= atomically . writeTBQueue subQ . \case
|
||||
Left e -> (corrId, connId, ERR e)
|
||||
Right (connId', resp) -> (corrId, connId', resp)
|
||||
|
||||
withStore ::
|
||||
AgentMonad m =>
|
||||
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => m' a) ->
|
||||
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) ->
|
||||
m a
|
||||
withStore action = do
|
||||
runExceptT (action `E.catch` handleInternal) >>= \case
|
||||
st <- asks store
|
||||
runExceptT (action st `E.catch` handleInternal) >>= \case
|
||||
Right c -> return c
|
||||
Left _ -> throwError STORE
|
||||
Left e -> throwError $ storeError e
|
||||
where
|
||||
handleInternal :: (MonadError StoreError m') => SomeException -> m' a
|
||||
handleInternal _ = throwError SEInternal
|
||||
-- TODO when parsing exception happens in store, the agent hangs;
|
||||
-- changing SQLError to SomeException does not help
|
||||
handleInternal :: (MonadError StoreError m') => SQLError -> m' a
|
||||
handleInternal e = throwError . SEInternal $ bshow e
|
||||
storeError :: StoreError -> AgentErrorType
|
||||
storeError = \case
|
||||
SEConnNotFound -> CONN NOT_FOUND
|
||||
SEConnDuplicate -> CONN DUPLICATE
|
||||
SEBadConnType CRcv -> CONN SIMPLEX
|
||||
SEBadConnType CSnd -> CONN SIMPLEX
|
||||
SEInvitationNotFound -> CMD PROHIBITED
|
||||
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
|
||||
-- | execute any SMP agent command
|
||||
processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent)
|
||||
processCommand c (connId, cmd) = case cmd of
|
||||
NEW (ACM cMode) -> second (INV . ACR cMode) <$> newConn c connId cMode
|
||||
JOIN (ACR _ cReq) connInfo -> (,OK) <$> joinConn c connId cReq connInfo
|
||||
LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK)
|
||||
ACPT invId ownCInfo -> (,OK) <$> acceptContact' c connId invId ownCInfo
|
||||
RJCT invId -> rejectContact' c connId invId $> (connId, OK)
|
||||
SUB -> subscribeConnection' c connId $> (connId, OK)
|
||||
SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody
|
||||
ACK msgId -> ackMessage' c connId msgId $> (connId, OK)
|
||||
OFF -> suspendConnection' c connId $> (connId, OK)
|
||||
DEL -> deleteConnection' c connId $> (connId, OK)
|
||||
|
||||
newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequest c)
|
||||
newConn c connId cMode = do
|
||||
srv <- getSMPServer
|
||||
(rq, qUri, encryptKey) <- newRcvQueue c srv
|
||||
g <- asks idsDrg
|
||||
let cData = ConnData {connId}
|
||||
connId' <- withStore $ \st -> createRcvConn st g cData rq cMode
|
||||
addSubscription c rq connId'
|
||||
let crData = ConnReqData simplexChat [qUri] encryptKey
|
||||
pure . (connId',) $ case cMode of
|
||||
SCMInvitation -> CRInvitation crData
|
||||
SCMContact -> CRContact crData
|
||||
|
||||
joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequest c -> ConnInfo -> m ConnId
|
||||
joinConn c connId (CRInvitation (ConnReqData _ (qUri :| _) encryptKey)) cInfo = do
|
||||
(sq, senderKey, verifyKey) <- newSndQueue qUri encryptKey
|
||||
g <- asks idsDrg
|
||||
cfg <- asks config
|
||||
let cData = ConnData {connId}
|
||||
connId' <- withStore $ \st -> createSndConn st g cData sq
|
||||
confirmQueue c sq senderKey cInfo
|
||||
activateQueueJoining c connId' sq verifyKey $ retryInterval cfg
|
||||
pure connId'
|
||||
joinConn c connId (CRContact (ConnReqData _ (qUri :| _) encryptKey)) cInfo = do
|
||||
(connId', cReq) <- newConn c connId SCMInvitation
|
||||
sendInvitation c qUri encryptKey cReq cInfo
|
||||
pure connId'
|
||||
|
||||
activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m ()
|
||||
activateQueueJoining c connId sq verifyKey retryInterval =
|
||||
activateQueue c connId sq verifyKey retryInterval createReplyQueue
|
||||
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
|
||||
createReplyQueue :: m ()
|
||||
createReplyQueue = do
|
||||
srv <- getSMPServer
|
||||
(rq, qUri', encryptKey) <- newRcvQueue c srv
|
||||
addSubscription c rq connId
|
||||
withStore $ \st -> upgradeSndConnToDuplex st connId rq
|
||||
sendControlMessage c sq . REPLY $ CRInvitation $ ConnReqData CRSSimplex [qUri'] encryptKey
|
||||
|
||||
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
|
||||
-- | Approve confirmation (LET command) in Reader monad
|
||||
allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m ()
|
||||
allowConnection' c connId confId ownConnInfo = do
|
||||
withStore (`getConn` connId) >>= \case
|
||||
SomeConn _ (RcvConnection _ rq) -> do
|
||||
AcceptedConfirmation {senderKey} <- withStore $ \st -> acceptConfirmation st confId ownConnInfo
|
||||
processConfirmation c rq senderKey
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
|
||||
subscribeConnection :: ConnAlias -> m ()
|
||||
subscribeConnection cAlias =
|
||||
withStore (getConn st cAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> subscribe rq
|
||||
SomeConn _ (RcvConnection _ rq) -> subscribe rq
|
||||
-- TODO possibly there should be a separate error type trying
|
||||
-- TODO to send the message to the connection without RcvQueue
|
||||
_ -> throwError PROHIBITED
|
||||
-- | Accept contact (ACPT command) in Reader monad
|
||||
acceptContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId
|
||||
acceptContact' c connId invId ownConnInfo = do
|
||||
Invitation {contactConnId, connReq} <- withStore (`getInvitation` invId)
|
||||
withStore (`getConn` contactConnId) >>= \case
|
||||
SomeConn _ ContactConnection {} -> do
|
||||
withStore $ \st -> acceptInvitation st invId ownConnInfo
|
||||
joinConn c connId connReq ownConnInfo
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
|
||||
-- | Reject contact (RJCT command) in Reader monad
|
||||
rejectContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> m ()
|
||||
rejectContact' _ contactConnId invId =
|
||||
withStore $ \st -> deleteInvitation st contactConnId invId
|
||||
|
||||
processConfirmation :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m ()
|
||||
processConfirmation c rq sndKey = do
|
||||
withStore $ \st -> setRcvQueueStatus st rq Confirmed
|
||||
secureQueue c rq sndKey
|
||||
withStore $ \st -> setRcvQueueStatus st rq Secured
|
||||
|
||||
-- | Subscribe to receive connection messages (SUB command) in Reader monad
|
||||
subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m ()
|
||||
subscribeConnection' c connId =
|
||||
withStore (`getConn` connId) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq sq) -> do
|
||||
resumeMsgDelivery c connId sq
|
||||
case status (sq :: SndQueue) of
|
||||
Confirmed -> withVerifyKey sq $ \verifyKey -> do
|
||||
conf <- withStore (`getAcceptedConfirmation` connId)
|
||||
secureQueue c rq $ senderKey (conf :: AcceptedConfirmation)
|
||||
withStore $ \st -> setRcvQueueStatus st rq Secured
|
||||
activateSecuredQueue rq sq verifyKey
|
||||
Secured -> withVerifyKey sq $ activateSecuredQueue rq sq
|
||||
Active -> subscribeQueue c rq connId
|
||||
_ -> throwError $ INTERNAL "unexpected queue status"
|
||||
SomeConn _ (SndConnection _ sq) -> do
|
||||
resumeMsgDelivery c connId sq
|
||||
case status (sq :: SndQueue) of
|
||||
Confirmed -> withVerifyKey sq $ \verifyKey ->
|
||||
activateQueueJoining c connId sq verifyKey =<< resumeInterval
|
||||
Active -> throwError $ CONN SIMPLEX
|
||||
_ -> throwError $ INTERNAL "unexpected queue status"
|
||||
SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId
|
||||
SomeConn _ (ContactConnection _ rq) -> subscribeQueue c rq connId
|
||||
where
|
||||
withVerifyKey :: SndQueue -> (C.PublicKey -> m ()) -> m ()
|
||||
withVerifyKey sq action =
|
||||
let err = throwError $ INTERNAL "missing signing key public counterpart"
|
||||
in maybe err action . C.publicKey $ signKey sq
|
||||
activateSecuredQueue :: RcvQueue -> SndQueue -> C.PublicKey -> m ()
|
||||
activateSecuredQueue rq sq verifyKey = do
|
||||
activateQueueInitiating c connId sq verifyKey =<< resumeInterval
|
||||
subscribeQueue c rq connId
|
||||
resumeInterval :: m RetryInterval
|
||||
resumeInterval = do
|
||||
r <- asks $ retryInterval . config
|
||||
pure r {initialInterval = 5_000_000}
|
||||
|
||||
-- | Send message to the connection (SEND command) in Reader monad
|
||||
sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId
|
||||
sendMessage' c connId msg =
|
||||
withStore (`getConn` connId) >>= \case
|
||||
SomeConn _ (DuplexConnection _ _ sq) -> enqueueMessage sq
|
||||
SomeConn _ (SndConnection _ sq) -> enqueueMessage sq
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
where
|
||||
enqueueMessage :: SndQueue -> m AgentMsgId
|
||||
enqueueMessage sq = do
|
||||
resumeMsgDelivery c connId sq
|
||||
msgId <- storeSentMsg
|
||||
queuePendingMsgs c connId sq [msgId]
|
||||
pure $ unId msgId
|
||||
where
|
||||
subscribe rq = subscribeQueue c rq cAlias >> respond' cAlias OK
|
||||
storeSentMsg :: m InternalId
|
||||
storeSentMsg = do
|
||||
internalTs <- liftIO getCurrentTime
|
||||
withStore $ \st -> do
|
||||
(internalId, internalSndId, previousMsgHash) <- updateSndIds st connId
|
||||
let msgBody =
|
||||
serializeSMPMessage
|
||||
SMPMessage
|
||||
{ senderMsgId = unSndId internalSndId,
|
||||
senderTimestamp = internalTs,
|
||||
previousMsgHash,
|
||||
agentMessage = A_MSG msg
|
||||
}
|
||||
internalHash = C.sha256Hash msgBody
|
||||
msgData = SndMsgData {..}
|
||||
createSndMsg st connId msgData
|
||||
pure internalId
|
||||
|
||||
-- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct
|
||||
subscribeAll :: m ()
|
||||
subscribeAll = withStore (getAllConnAliases st) >>= mapM_ subscribeConnection
|
||||
resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
|
||||
resumeMsgDelivery c connId sq@SndQueue {server, sndId} = do
|
||||
let qKey = (connId, server, sndId)
|
||||
unlessM (queueDelivering qKey) $
|
||||
async (runSmpQueueMsgDelivery c connId sq)
|
||||
>>= atomically . modifyTVar (smpQueueMsgDeliveries c) . M.insert qKey
|
||||
unlessM connQueued $
|
||||
withStore (`getPendingMsgs` connId)
|
||||
>>= queuePendingMsgs c connId sq
|
||||
where
|
||||
queueDelivering qKey = isJust . M.lookup qKey <$> readTVarIO (smpQueueMsgDeliveries c)
|
||||
connQueued =
|
||||
atomically $
|
||||
isJust
|
||||
<$> stateTVar
|
||||
(connMsgsQueued c)
|
||||
(\m -> (M.lookup connId m, M.insert connId True m))
|
||||
|
||||
sendMessage :: MsgBody -> m ()
|
||||
sendMessage msgBody =
|
||||
withStore (getConn st connAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq
|
||||
SomeConn _ (SndConnection _ sq) -> sendMsg sq
|
||||
-- TODO possibly there should be a separate error type trying
|
||||
-- TODO to send the message to the connection without SndQueue
|
||||
_ -> throwError PROHIBITED -- NOT_READY ?
|
||||
where
|
||||
sendMsg sq = do
|
||||
senderTs <- liftIO getCurrentTime
|
||||
senderId <- withStore $ createSndMsg st connAlias msgBody senderTs
|
||||
sendAgentMessage c sq senderTs $ A_MSG msgBody
|
||||
respond $ SENT (unId senderId)
|
||||
queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> [InternalId] -> m ()
|
||||
queuePendingMsgs c connId sq msgIds = atomically $ do
|
||||
q <- getPendingMsgQ c connId sq
|
||||
mapM_ (writeTQueue q) msgIds
|
||||
|
||||
suspendConnection :: m ()
|
||||
suspendConnection =
|
||||
withStore (getConn st connAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> suspend rq
|
||||
SomeConn _ (RcvConnection _ rq) -> suspend rq
|
||||
_ -> throwError PROHIBITED
|
||||
where
|
||||
suspend rq = suspendQueue c rq >> respond OK
|
||||
getPendingMsgQ :: AgentClient -> ConnId -> SndQueue -> STM (TQueue InternalId)
|
||||
getPendingMsgQ c connId SndQueue {server, sndId} = do
|
||||
let qKey = (connId, server, sndId)
|
||||
maybe (newMsgQueue qKey) pure . M.lookup qKey =<< readTVar (smpQueueMsgQueues c)
|
||||
where
|
||||
newMsgQueue qKey = do
|
||||
mq <- newTQueue
|
||||
modifyTVar (smpQueueMsgQueues c) $ M.insert qKey mq
|
||||
pure mq
|
||||
|
||||
deleteConnection :: m ()
|
||||
deleteConnection =
|
||||
withStore (getConn st connAlias) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> delete rq
|
||||
SomeConn _ (RcvConnection _ rq) -> delete rq
|
||||
_ -> throwError PROHIBITED
|
||||
where
|
||||
delete rq = do
|
||||
deleteQueue c rq
|
||||
removeSubscription c connAlias
|
||||
withStore (deleteConn st connAlias)
|
||||
respond OK
|
||||
runSmpQueueMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
|
||||
runSmpQueueMsgDelivery c@AgentClient {subQ} connId sq = do
|
||||
mq <- atomically $ getPendingMsgQ c connId sq
|
||||
ri <- asks $ reconnectInterval . config
|
||||
forever $ do
|
||||
msgId <- atomically $ readTQueue mq
|
||||
let mId = unId msgId
|
||||
withStore (\st -> E.try $ getPendingMsgData st connId msgId) >>= \case
|
||||
Left (e :: E.SomeException) ->
|
||||
notify $ MERR mId (INTERNAL $ show e)
|
||||
Right msgBody -> do
|
||||
withRetryInterval ri $ \loop -> do
|
||||
tryError (sendAgentMessage c sq msgBody) >>= \case
|
||||
Left e -> case e of
|
||||
SMP SMP.QUOTA -> loop
|
||||
SMP {} -> notify $ MERR mId e
|
||||
CMD {} -> notify $ MERR mId e
|
||||
_ -> loop
|
||||
Right () -> do
|
||||
notify $ SENT mId
|
||||
withStore $ \st -> updateSndMsgStatus st connId msgId SndMsgSent
|
||||
where
|
||||
notify :: ACommand 'Agent -> m ()
|
||||
notify cmd = atomically $ writeTBQueue subQ ("", connId, cmd)
|
||||
|
||||
sendReplyQInfo :: SMPServer -> SndQueue -> m ()
|
||||
sendReplyQInfo srv sq = do
|
||||
(rq, qInfo) <- newReceiveQueue c srv connAlias
|
||||
withStore $ upgradeSndConnToDuplex st connAlias rq
|
||||
senderTs <- liftIO getCurrentTime
|
||||
sendAgentMessage c sq senderTs $ REPLY qInfo
|
||||
ackMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> AgentMsgId -> m ()
|
||||
ackMessage' c connId msgId = do
|
||||
withStore (`getConn` connId) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> ack rq
|
||||
SomeConn _ (RcvConnection _ rq) -> ack rq
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
where
|
||||
ack :: RcvQueue -> m ()
|
||||
ack rq = do
|
||||
let mId = InternalId msgId
|
||||
withStore $ \st -> checkRcvMsg st connId mId
|
||||
sendAck c rq
|
||||
withStore $ \st -> updateRcvMsgAck st connId mId
|
||||
|
||||
respond :: ACommand 'Agent -> m ()
|
||||
respond = respond' connAlias
|
||||
-- | Suspend SMP agent connection (OFF command) in Reader monad
|
||||
suspendConnection' :: AgentMonad m => AgentClient -> ConnId -> m ()
|
||||
suspendConnection' c connId =
|
||||
withStore (`getConn` connId) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> suspendQueue c rq
|
||||
SomeConn _ (RcvConnection _ rq) -> suspendQueue c rq
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
|
||||
respond' :: ConnAlias -> ACommand 'Agent -> m ()
|
||||
respond' cAlias resp = atomically $ writeTBQueue sndQ (corrId, cAlias, resp)
|
||||
-- | Delete SMP agent connection (DEL command) in Reader monad
|
||||
deleteConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m ()
|
||||
deleteConnection' c connId =
|
||||
withStore (`getConn` connId) >>= \case
|
||||
SomeConn _ (DuplexConnection _ rq _) -> delete rq
|
||||
SomeConn _ (RcvConnection _ rq) -> delete rq
|
||||
SomeConn _ (ContactConnection _ rq) -> delete rq
|
||||
SomeConn _ (SndConnection _ _) -> withStore (`deleteConn` connId)
|
||||
where
|
||||
delete :: RcvQueue -> m ()
|
||||
delete rq = do
|
||||
deleteQueue c rq
|
||||
removeSubscription c connId
|
||||
withStore (`deleteConn` connId)
|
||||
|
||||
subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()
|
||||
subscriber c@AgentClient {msgQ} st = forever $ do
|
||||
-- TODO this will only process messages and notifications
|
||||
getSMPServer :: AgentMonad m => m SMPServer
|
||||
getSMPServer =
|
||||
asks (smpServers . config) >>= \case
|
||||
srv :| [] -> pure srv
|
||||
servers -> do
|
||||
gen <- asks randomServer
|
||||
i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1)
|
||||
pure $ servers L.!! i
|
||||
|
||||
sendControlMessage :: AgentMonad m => AgentClient -> SndQueue -> AMessage -> m ()
|
||||
sendControlMessage c sq agentMessage = do
|
||||
senderTimestamp <- liftIO getCurrentTime
|
||||
sendAgentMessage c sq . serializeSMPMessage $
|
||||
SMPMessage
|
||||
{ senderMsgId = 0,
|
||||
senderTimestamp,
|
||||
previousMsgHash = "",
|
||||
agentMessage
|
||||
}
|
||||
|
||||
subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
|
||||
subscriber c@AgentClient {msgQ} = forever $ do
|
||||
t <- atomically $ readTBQueue msgQ
|
||||
runExceptT (processSMPTransmission c st t) >>= \case
|
||||
withAgentLock c (runExceptT $ processSMPTransmission c 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, decryptKey, status} <- withStore $ getRcvQueue st srv rId
|
||||
case cmd of
|
||||
SMP.MSG srvMsgId srvTs msgBody -> do
|
||||
-- TODO deduplicate with previously received
|
||||
agentMsg <- liftEither . parseSMPMessage =<< decryptMessage decryptKey msgBody
|
||||
case agentMsg of
|
||||
SMPConfirmation senderKey -> do
|
||||
processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SMPServerTransmission -> m ()
|
||||
processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do
|
||||
withStore (\st -> getRcvConn st srv rId) >>= \case
|
||||
SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq
|
||||
SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq
|
||||
SomeConn SCContact (ContactConnection cData rq) -> processSMP SCContact cData rq
|
||||
_ -> atomically $ writeTBQueue subQ ("", "", ERR $ CONN NOT_FOUND)
|
||||
where
|
||||
processSMP :: SConnType c -> ConnData -> RcvQueue -> m ()
|
||||
processSMP cType ConnData {connId} rq@RcvQueue {status} =
|
||||
case cmd of
|
||||
SMP.MSG srvMsgId srvTs msgBody -> do
|
||||
-- TODO deduplicate with previously received
|
||||
msg <- decryptAndVerify rq msgBody
|
||||
let msgHash = C.sha256Hash msg
|
||||
case parseSMPMessage msg of
|
||||
Left e -> notify (ERR e) >> sendAck c rq
|
||||
Right (SMPConfirmation senderKey cInfo) -> smpConfirmation senderKey cInfo >> sendAck c rq
|
||||
Right SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} ->
|
||||
case agentMessage of
|
||||
HELLO verifyKey _ -> helloMsg verifyKey msgBody >> sendAck c rq
|
||||
REPLY cReq -> replyMsg cReq >> sendAck c rq
|
||||
A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash
|
||||
A_INV cReq cInfo -> smpInvitation cReq cInfo >> sendAck c rq
|
||||
SMP.END -> do
|
||||
removeSubscription c connId
|
||||
logServer "<--" c srv rId "END"
|
||||
notify END
|
||||
_ -> do
|
||||
logServer "<--" c srv rId $ "unexpected: " <> bshow cmd
|
||||
notify . ERR $ BROKER UNEXPECTED
|
||||
where
|
||||
notify :: ACommand 'Agent -> m ()
|
||||
notify msg = atomically $ writeTBQueue subQ ("", connId, msg)
|
||||
|
||||
prohibited :: m ()
|
||||
prohibited = notify . ERR $ AGENT A_PROHIBITED
|
||||
|
||||
smpConfirmation :: SenderPublicKey -> ConnInfo -> m ()
|
||||
smpConfirmation senderKey cInfo = 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
|
||||
sendAck c rq
|
||||
s ->
|
||||
-- TODO maybe send notification to the user
|
||||
liftIO . putStrLn $ "unexpected SMP confirmation, queue status " <> show s
|
||||
SMPMessage {agentMessage, senderMsgId, senderTimestamp} ->
|
||||
case agentMessage of
|
||||
HELLO _verifyKey _ -> do
|
||||
logServer "<--" c srv rId "MSG <HELLO>"
|
||||
-- TODO send status update to the user?
|
||||
withStore $ setRcvQueueStatus st rq Active
|
||||
sendAck c rq
|
||||
REPLY qInfo -> do
|
||||
logServer "<--" c srv rId "MSG <REPLY>"
|
||||
-- TODO move senderKey inside SndQueue
|
||||
(sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias
|
||||
withStore $ upgradeRcvConnToDuplex st connAlias sq
|
||||
connectToSendQueue c st sq senderKey verifyKey
|
||||
notify connAlias CON
|
||||
sendAck c rq
|
||||
A_MSG body -> do
|
||||
logServer "<--" c srv rId "MSG <MSG>"
|
||||
-- TODO check message status
|
||||
recipientTs <- liftIO getCurrentTime
|
||||
let m_sender = (senderMsgId, senderTimestamp)
|
||||
let m_broker = (srvMsgId, srvTs)
|
||||
recipientId <- withStore $ createRcvMsg st connAlias body recipientTs m_sender m_broker
|
||||
notify connAlias $
|
||||
MSG
|
||||
{ m_status = MsgOk,
|
||||
m_recipient = (unId recipientId, recipientTs),
|
||||
m_sender,
|
||||
m_broker,
|
||||
m_body = body
|
||||
}
|
||||
sendAck c rq
|
||||
return ()
|
||||
SMP.END -> do
|
||||
removeSubscription c connAlias
|
||||
logServer "<--" c srv rId "END"
|
||||
notify connAlias END
|
||||
_ -> logServer "<--" c srv rId $ "unexpected:" <> (B.pack . show) cmd
|
||||
New -> case cType of
|
||||
SCRcv -> do
|
||||
g <- asks idsDrg
|
||||
let newConfirmation = NewConfirmation {connId, senderKey, senderConnInfo = cInfo}
|
||||
confId <- withStore $ \st -> createConfirmation st g newConfirmation
|
||||
notify $ CONF confId cInfo
|
||||
SCDuplex -> do
|
||||
notify $ INFO cInfo
|
||||
processConfirmation c rq senderKey
|
||||
_ -> prohibited
|
||||
_ -> prohibited
|
||||
|
||||
helloMsg :: SenderPublicKey -> ByteString -> m ()
|
||||
helloMsg verifyKey msgBody = do
|
||||
logServer "<--" c srv rId "MSG <HELLO>"
|
||||
case status of
|
||||
Active -> prohibited
|
||||
_ -> do
|
||||
void $ verifyMessage (Just verifyKey) msgBody
|
||||
withStore $ \st -> setRcvQueueActive st rq verifyKey
|
||||
case cType of
|
||||
SCDuplex -> notifyConnected c connId
|
||||
_ -> pure ()
|
||||
|
||||
replyMsg :: ConnectionRequest 'CMInvitation -> m ()
|
||||
replyMsg (CRInvitation (ConnReqData _ (qUri :| _) encryptKey)) = do
|
||||
logServer "<--" c srv rId "MSG <REPLY>"
|
||||
case cType of
|
||||
SCRcv -> do
|
||||
AcceptedConfirmation {ownConnInfo} <- withStore (`getAcceptedConfirmation` connId)
|
||||
(sq, senderKey, verifyKey) <- newSndQueue qUri encryptKey
|
||||
withStore $ \st -> upgradeRcvConnToDuplex st connId sq
|
||||
confirmQueue c sq senderKey ownConnInfo
|
||||
withStore (`removeConfirmations` connId)
|
||||
cfg <- asks config
|
||||
activateQueueInitiating c connId sq verifyKey $ retryInterval cfg
|
||||
_ -> prohibited
|
||||
|
||||
agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m ()
|
||||
agentClientMsg externalPrevSndHash sender broker msgBody internalHash = do
|
||||
logServer "<--" c srv rId "MSG <MSG>"
|
||||
internalTs <- liftIO getCurrentTime
|
||||
(internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId)
|
||||
let integrity = checkMsgIntegrity prevExtSndId (fst sender) prevRcvMsgHash externalPrevSndHash
|
||||
recipient = (unId internalId, internalTs)
|
||||
msgMeta = MsgMeta {integrity, recipient, sender, broker}
|
||||
rcvMsg = RcvMsgData {..}
|
||||
withStore $ \st -> createRcvMsg st connId rcvMsg
|
||||
notify $ MSG msgMeta msgBody
|
||||
|
||||
smpInvitation :: ConnectionRequest 'CMInvitation -> ConnInfo -> m ()
|
||||
smpInvitation connReq cInfo = do
|
||||
logServer "<--" c srv rId "MSG <KEY>"
|
||||
case cType of
|
||||
SCContact -> do
|
||||
g <- asks idsDrg
|
||||
let newInv = NewInvitation {contactConnId = connId, connReq, recipientConnInfo = cInfo}
|
||||
invId <- withStore $ \st -> createInvitation st g newInv
|
||||
notify $ REQ invId cInfo
|
||||
_ -> prohibited
|
||||
|
||||
checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity
|
||||
checkMsgIntegrity prevExtSndId extSndId internalPrevMsgHash receivedPrevMsgHash
|
||||
| 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
|
||||
|
||||
confirmQueue :: AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> ConnInfo -> m ()
|
||||
confirmQueue c sq senderKey cInfo = do
|
||||
sendConfirmation c sq senderKey cInfo
|
||||
withStore $ \st -> setSndQueueStatus st sq Confirmed
|
||||
|
||||
activateQueueInitiating :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m ()
|
||||
activateQueueInitiating c connId sq verifyKey retryInterval =
|
||||
activateQueue c connId sq verifyKey retryInterval $ notifyConnected c connId
|
||||
|
||||
activateQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () -> m ()
|
||||
activateQueue c connId sq verifyKey retryInterval afterActivation =
|
||||
getActivation c connId >>= \case
|
||||
Nothing -> async runActivation >>= addActivation c connId
|
||||
Just _ -> pure ()
|
||||
where
|
||||
notify :: ConnAlias -> ACommand 'Agent -> m ()
|
||||
notify connAlias msg = atomically $ writeTBQueue sndQ ("", connAlias, msg)
|
||||
runActivation :: m ()
|
||||
runActivation = do
|
||||
sendHello c sq verifyKey retryInterval
|
||||
withStore $ \st -> setSndQueueStatus st sq Active
|
||||
removeActivation c connId
|
||||
removeVerificationKey
|
||||
afterActivation
|
||||
removeVerificationKey :: m ()
|
||||
removeVerificationKey =
|
||||
let safeSignKey = C.removePublicKey $ signKey sq
|
||||
in withStore $ \st -> updateSignKey st sq safeSignKey
|
||||
|
||||
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
|
||||
notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m ()
|
||||
notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON)
|
||||
|
||||
decryptMessage :: (MonadUnliftIO m, MonadError AgentErrorType m) => DecryptionKey -> ByteString -> m ByteString
|
||||
decryptMessage decryptKey msg = liftError CRYPTO $ C.decrypt decryptKey msg
|
||||
|
||||
newSendQueue ::
|
||||
(MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> ConnAlias -> m (SndQueue, SenderPublicKey, VerificationKey)
|
||||
newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do
|
||||
newSndQueue ::
|
||||
(MonadUnliftIO m, MonadReader Env m) => SMPQueueUri -> EncryptionKey -> m (SndQueue, SenderPublicKey, VerificationKey)
|
||||
newSndQueue (SMPQueueUri smpServer senderId _) encryptKey = do
|
||||
size <- asks $ rsaKeySize . config
|
||||
(senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size
|
||||
(verifyKey, signKey) <- liftIO $ C.generateKeyPair size
|
||||
@@ -303,7 +673,6 @@ newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do
|
||||
SndQueue
|
||||
{ server = smpServer,
|
||||
sndId = senderId,
|
||||
connAlias,
|
||||
sndPrivateKey,
|
||||
encryptKey,
|
||||
signKey,
|
||||
|
||||
@@ -11,22 +11,33 @@ module Simplex.Messaging.Agent.Client
|
||||
( AgentClient (..),
|
||||
newAgentClient,
|
||||
AgentMonad,
|
||||
getSMPServerClient,
|
||||
closeSMPServerClients,
|
||||
newReceiveQueue,
|
||||
withAgentLock,
|
||||
closeAgentClient,
|
||||
newRcvQueue,
|
||||
subscribeQueue,
|
||||
addSubscription,
|
||||
sendConfirmation,
|
||||
sendInvitation,
|
||||
RetryInterval (..),
|
||||
sendHello,
|
||||
secureQueue,
|
||||
sendAgentMessage,
|
||||
decryptAndVerify,
|
||||
verifyMessage,
|
||||
sendAck,
|
||||
suspendQueue,
|
||||
deleteQueue,
|
||||
logServer,
|
||||
removeSubscription,
|
||||
cryptoError,
|
||||
addActivation,
|
||||
getActivation,
|
||||
removeActivation,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.Async (Async, async, uninterruptibleCancel)
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
@@ -37,44 +48,61 @@ 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.Maybe (isNothing)
|
||||
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.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
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 (liftError)
|
||||
import UnliftIO.Concurrent
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Util (bshow, liftEitherError, liftError)
|
||||
import UnliftIO.Exception (IOException)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
data AgentClient = AgentClient
|
||||
{ rcvQ :: TBQueue (ATransmission 'Client),
|
||||
sndQ :: TBQueue (ATransmission 'Agent),
|
||||
subQ :: TBQueue (ATransmission 'Agent),
|
||||
msgQ :: TBQueue SMPServerTransmission,
|
||||
smpClients :: TVar (Map SMPServer SMPClient),
|
||||
subscrSrvrs :: TVar (Map SMPServer (Set ConnAlias)),
|
||||
subscrConns :: TVar (Map ConnAlias SMPServer),
|
||||
clientId :: Int
|
||||
subscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)),
|
||||
subscrConns :: TVar (Map ConnId SMPServer),
|
||||
activations :: TVar (Map ConnId (Async ())), -- activations of send queues in progress
|
||||
connMsgsQueued :: TVar (Map ConnId Bool),
|
||||
smpQueueMsgQueues :: TVar (Map (ConnId, SMPServer, SMP.SenderId) (TQueue InternalId)),
|
||||
smpQueueMsgDeliveries :: TVar (Map (ConnId, SMPServer, SMP.SenderId) (Async ())),
|
||||
reconnections :: TVar [Async ()],
|
||||
clientId :: Int,
|
||||
agentEnv :: Env,
|
||||
smpSubscriber :: Async (),
|
||||
lock :: TMVar ()
|
||||
}
|
||||
|
||||
newAgentClient :: TVar Int -> AgentConfig -> STM AgentClient
|
||||
newAgentClient cc AgentConfig {tbqSize} = do
|
||||
rcvQ <- newTBQueue tbqSize
|
||||
sndQ <- newTBQueue tbqSize
|
||||
msgQ <- newTBQueue tbqSize
|
||||
newAgentClient :: Env -> STM AgentClient
|
||||
newAgentClient agentEnv = do
|
||||
let qSize = tbqSize $ config agentEnv
|
||||
rcvQ <- newTBQueue qSize
|
||||
subQ <- newTBQueue qSize
|
||||
msgQ <- newTBQueue qSize
|
||||
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}
|
||||
activations <- newTVar M.empty
|
||||
connMsgsQueued <- newTVar M.empty
|
||||
smpQueueMsgQueues <- newTVar M.empty
|
||||
smpQueueMsgDeliveries <- newTVar M.empty
|
||||
reconnections <- newTVar []
|
||||
clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1)
|
||||
lock <- newTMVar ()
|
||||
return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, connMsgsQueued, smpQueueMsgQueues, smpQueueMsgDeliveries, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock}
|
||||
|
||||
-- | Agent monad with MonadReader Env and MonadError AgentErrorType
|
||||
type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m)
|
||||
|
||||
getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient
|
||||
@@ -86,65 +114,119 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv =
|
||||
newSMPClient = do
|
||||
smp <- connectClient
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
|
||||
-- TODO how can agent know client lost the connection?
|
||||
atomically . modifyTVar smpClients $ M.insert srv smp
|
||||
return smp
|
||||
|
||||
connectClient :: m SMPClient
|
||||
connectClient = do
|
||||
cfg <- asks $ smpCfg . config
|
||||
liftIO (getSMPClient srv cfg msgQ clientDisconnected)
|
||||
`E.catch` \(_ :: IOException) -> throwError (BROKER smpErrTCPConnection)
|
||||
u <- askUnliftIO
|
||||
liftEitherError smpClientError (getSMPClient srv cfg msgQ $ clientDisconnected u)
|
||||
`E.catch` internalError
|
||||
where
|
||||
internalError :: IOException -> m SMPClient
|
||||
internalError = throwError . INTERNAL . show
|
||||
|
||||
clientDisconnected :: IO ()
|
||||
clientDisconnected = do
|
||||
removeSubs >>= mapM_ (mapM_ notifySub)
|
||||
clientDisconnected :: UnliftIO m -> IO ()
|
||||
clientDisconnected u = do
|
||||
removeClientSubs >>= (`forM_` serverDown u)
|
||||
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
||||
|
||||
removeSubs :: IO (Maybe (Set ConnAlias))
|
||||
removeSubs = atomically $ do
|
||||
removeClientSubs :: IO (Maybe (Map ConnId RcvQueue))
|
||||
removeClientSubs = 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
|
||||
modifyTVar (subscrConns c) $ maybe id (deleteKeys . M.keysSet) 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)
|
||||
serverDown :: UnliftIO m -> Map ConnId RcvQueue -> IO ()
|
||||
serverDown u cs = unless (M.null cs) $ do
|
||||
mapM_ (notifySub DOWN) $ M.keysSet cs
|
||||
a <- async . unliftIO u $ tryReconnectClient cs
|
||||
atomically $ modifyTVar (reconnections c) (a :)
|
||||
|
||||
closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m ()
|
||||
closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient
|
||||
tryReconnectClient :: Map ConnId RcvQueue -> m ()
|
||||
tryReconnectClient cs = do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \loop ->
|
||||
reconnectClient cs `catchError` const loop
|
||||
|
||||
withSMP :: forall a m. AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> ExceptT SMPClientError IO a) -> m a
|
||||
withSMP c srv action =
|
||||
(getSMPServerClient c srv >>= runAction) `catchError` logServerError
|
||||
reconnectClient :: Map ConnId RcvQueue -> m ()
|
||||
reconnectClient cs = do
|
||||
withAgentLock c . withSMP c srv $ \smp -> do
|
||||
subs <- readTVarIO $ subscrConns c
|
||||
forM_ (M.toList cs) $ \(connId, rq@RcvQueue {rcvPrivateKey, rcvId}) ->
|
||||
when (isNothing $ M.lookup connId subs) $ do
|
||||
subscribeSMPQueue smp rcvPrivateKey rcvId
|
||||
`catchError` \case
|
||||
SMPServerError e -> liftIO $ notifySub (ERR $ SMP e) connId
|
||||
e -> throwError e
|
||||
addSubscription c rq connId
|
||||
liftIO $ notifySub UP connId
|
||||
|
||||
notifySub :: ACommand 'Agent -> ConnId -> IO ()
|
||||
notifySub cmd connId = atomically $ writeTBQueue (subQ c) ("", connId, cmd)
|
||||
|
||||
closeAgentClient :: MonadUnliftIO m => AgentClient -> m ()
|
||||
closeAgentClient c = liftIO $ do
|
||||
closeSMPServerClients c
|
||||
cancelActions $ activations c
|
||||
cancelActions $ reconnections c
|
||||
cancelActions $ smpQueueMsgDeliveries c
|
||||
|
||||
closeSMPServerClients :: AgentClient -> IO ()
|
||||
closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ closeSMPClient
|
||||
|
||||
cancelActions :: Foldable f => TVar (f (Async ())) -> IO ()
|
||||
cancelActions as = readTVarIO as >>= mapM_ uninterruptibleCancel
|
||||
|
||||
withAgentLock :: MonadUnliftIO m => AgentClient -> m a -> m a
|
||||
withAgentLock AgentClient {lock} =
|
||||
E.bracket_
|
||||
(void . atomically $ takeTMVar lock)
|
||||
(atomically $ putTMVar lock ())
|
||||
|
||||
withSMP_ :: forall a m. AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> m a) -> m a
|
||||
withSMP_ c srv action =
|
||||
(getSMPServerClient c srv >>= action) `catchError` logServerError
|
||||
where
|
||||
runAction :: SMPClient -> m a
|
||||
runAction smp = liftError smpClientError $ action smp
|
||||
|
||||
smpClientError :: SMPClientError -> AgentErrorType
|
||||
smpClientError = \case
|
||||
SMPServerError e -> SMP e
|
||||
-- TODO handle other errors
|
||||
_ -> INTERNAL
|
||||
|
||||
logServerError :: AgentErrorType -> m a
|
||||
logServerError e = do
|
||||
logServer "<--" c srv "" $ (B.pack . show) e
|
||||
logServer "<--" c srv "" $ bshow e
|
||||
throwError e
|
||||
|
||||
withLogSMP :: AgentMonad m => AgentClient -> SMPServer -> QueueId -> ByteString -> (SMPClient -> ExceptT SMPClientError IO a) -> m a
|
||||
withLogSMP c srv qId cmdStr action = do
|
||||
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
|
||||
res <- withSMP_ c srv action
|
||||
logServer "<--" c srv qId "OK"
|
||||
return res
|
||||
|
||||
newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> ConnAlias -> m (RcvQueue, SMPQueueInfo)
|
||||
newReceiveQueue c srv connAlias = do
|
||||
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
|
||||
|
||||
newRcvQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueUri, EncryptionKey)
|
||||
newRcvQueue c srv = do
|
||||
size <- asks $ rsaKeySize . config
|
||||
(recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size
|
||||
logServer "-->" c srv "" "NEW"
|
||||
@@ -155,48 +237,54 @@ newReceiveQueue c srv connAlias = do
|
||||
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)
|
||||
pure (rq, SMPQueueUri srv sId reservedServerKey, encryptKey)
|
||||
|
||||
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnAlias -> m ()
|
||||
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connAlias = do
|
||||
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m ()
|
||||
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do
|
||||
withLogSMP c server rcvId "SUB" $ \smp ->
|
||||
subscribeSMPQueue smp rcvPrivateKey rcvId
|
||||
addSubscription c rq connAlias
|
||||
addSubscription c rq connId
|
||||
|
||||
addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnAlias -> m ()
|
||||
addSubscription c RcvQueue {server} connAlias = atomically $ do
|
||||
modifyTVar (subscrConns c) $ M.insert connAlias server
|
||||
addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m ()
|
||||
addSubscription c rq@RcvQueue {server} connId = atomically $ do
|
||||
modifyTVar (subscrConns c) $ M.insert connId 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
|
||||
addSub :: Maybe (Map ConnId RcvQueue) -> Map ConnId RcvQueue
|
||||
addSub (Just cs) = M.insert connId rq cs
|
||||
addSub _ = M.singleton connId rq
|
||||
|
||||
removeSubscription :: AgentMonad m => AgentClient -> ConnAlias -> m ()
|
||||
removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically $ do
|
||||
removeSubscription :: AgentMonad m => AgentClient -> ConnId -> m ()
|
||||
removeSubscription AgentClient {subscrConns, subscrSrvrs} connId = atomically $ do
|
||||
cs <- readTVar subscrConns
|
||||
writeTVar subscrConns $ M.delete connAlias cs
|
||||
writeTVar subscrConns $ M.delete connId cs
|
||||
mapM_
|
||||
(modifyTVar subscrSrvrs . M.alter (>>= delSub))
|
||||
(M.lookup connAlias cs)
|
||||
(M.lookup connId cs)
|
||||
where
|
||||
delSub :: Set ConnAlias -> Maybe (Set ConnAlias)
|
||||
delSub :: Map ConnId RcvQueue -> Maybe (Map ConnId RcvQueue)
|
||||
delSub cs =
|
||||
let cs' = S.delete connAlias cs
|
||||
in if S.null cs' then Nothing else Just cs'
|
||||
let cs' = M.delete connId cs
|
||||
in if M.null cs' then Nothing else Just cs'
|
||||
|
||||
addActivation :: MonadUnliftIO m => AgentClient -> ConnId -> Async () -> m ()
|
||||
addActivation c connId a = atomically . modifyTVar (activations c) $ M.insert connId a
|
||||
|
||||
getActivation :: MonadUnliftIO m => AgentClient -> ConnId -> m (Maybe (Async ()))
|
||||
getActivation c connId = M.lookup connId <$> readTVarIO (activations c)
|
||||
|
||||
removeActivation :: MonadUnliftIO m => AgentClient -> ConnId -> m ()
|
||||
removeActivation c connId = atomically . modifyTVar (activations c) $ M.delete connId
|
||||
|
||||
logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m ()
|
||||
logServer dir AgentClient {clientId} srv qId cmdStr =
|
||||
logInfo . decodeUtf8 $ B.unwords ["A", "(" <> (B.pack . show) clientId <> ")", dir, showServer srv, ":", logSecret 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)
|
||||
@@ -204,37 +292,51 @@ 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 SndQueue {server, sndId, encryptKey} senderKey = do
|
||||
msg <- mkConfirmation
|
||||
withLogSMP c server sndId "SEND <KEY>" $ \smp ->
|
||||
sendSMPMessage smp Nothing sndId msg
|
||||
sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> ConnInfo -> m ()
|
||||
sendConfirmation c sq@SndQueue {server, sndId} senderKey cInfo =
|
||||
withLogSMP_ c server sndId "SEND <KEY>" $ \smp -> do
|
||||
msg <- mkConfirmation smp
|
||||
liftSMP $ sendSMPMessage smp Nothing sndId msg
|
||||
where
|
||||
mkConfirmation :: m MsgBody
|
||||
mkConfirmation = do
|
||||
let msg = serializeSMPMessage $ SMPConfirmation senderKey
|
||||
paddedSize <- asks paddedMsgSize
|
||||
liftError CRYPTO $ C.encrypt encryptKey paddedSize msg
|
||||
mkConfirmation :: SMPClient -> m MsgBody
|
||||
mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey cInfo
|
||||
|
||||
sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> m ()
|
||||
sendHello c SndQueue {server, sndId, sndPrivateKey, encryptKey} verifyKey = do
|
||||
msg <- mkHello $ AckMode On
|
||||
withLogSMP c server sndId "SEND <HELLO> (retrying)" $
|
||||
send 20 msg
|
||||
where
|
||||
mkHello :: AckMode -> m ByteString
|
||||
mkHello ackMode = do
|
||||
senderTs <- liftIO getCurrentTime
|
||||
mkAgentMessage encryptKey senderTs $ HELLO verifyKey ackMode
|
||||
|
||||
send :: Int -> ByteString -> SMPClient -> ExceptT SMPClientError IO ()
|
||||
send 0 _ _ = throwE SMPResponseTimeout -- TODO different error
|
||||
send retry msg smp =
|
||||
sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> RetryInterval -> m ()
|
||||
sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey ri =
|
||||
withLogSMP_ c server sndId "SEND <HELLO> (retrying)" $ \smp -> do
|
||||
msg <- mkHello smp $ AckMode On
|
||||
liftSMP . withRetryInterval ri $ \loop ->
|
||||
sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case
|
||||
SMPServerError AUTH -> do
|
||||
threadDelay 100000
|
||||
send (retry - 1) msg smp
|
||||
SMPServerError AUTH -> loop
|
||||
e -> throwE e
|
||||
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
|
||||
}
|
||||
|
||||
sendInvitation :: forall m. AgentMonad m => AgentClient -> SMPQueueUri -> EncryptionKey -> ConnectionRequest 'CMInvitation -> ConnInfo -> m ()
|
||||
sendInvitation c SMPQueueUri {smpServer, senderId} encryptKey cReq connInfo = do
|
||||
withLogSMP_ c smpServer senderId "SEND <INV>" $ \smp -> do
|
||||
msg <- mkInvitation smp
|
||||
liftSMP $ sendSMPMessage smp Nothing senderId msg
|
||||
where
|
||||
mkInvitation :: SMPClient -> m ByteString
|
||||
mkInvitation smp = do
|
||||
senderTimestamp <- liftIO getCurrentTime
|
||||
encryptUnsigned smp encryptKey . serializeSMPMessage $
|
||||
SMPMessage
|
||||
{ senderMsgId = 0,
|
||||
senderTimestamp,
|
||||
previousMsgHash = "",
|
||||
agentMessage = A_INV cReq connInfo
|
||||
}
|
||||
|
||||
secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m ()
|
||||
secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey =
|
||||
@@ -256,21 +358,48 @@ deleteQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
|
||||
withLogSMP c server rcvId "DEL" $ \smp ->
|
||||
deleteSMPQueue smp rcvPrivateKey rcvId
|
||||
|
||||
sendAgentMessage :: AgentMonad m => AgentClient -> SndQueue -> SenderTimestamp -> AMessage -> m ()
|
||||
sendAgentMessage c SndQueue {server, sndId, sndPrivateKey, encryptKey} senderTs agentMsg = do
|
||||
msg <- mkAgentMessage encryptKey senderTs agentMsg
|
||||
withLogSMP c server sndId "SEND <message>" $ \smp ->
|
||||
sendSMPMessage smp (Just sndPrivateKey) sndId msg
|
||||
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'
|
||||
|
||||
mkAgentMessage :: AgentMonad m => EncryptionKey -> SenderTimestamp -> AMessage -> m ByteString
|
||||
mkAgentMessage encKey senderTs agentMessage = do
|
||||
let msg =
|
||||
serializeSMPMessage
|
||||
SMPMessage
|
||||
{ senderMsgId = 0,
|
||||
senderTimestamp = senderTs,
|
||||
previousMsgHash = "1234", -- TODO hash of the previous message
|
||||
agentMessage
|
||||
}
|
||||
paddedSize <- asks paddedMsgSize
|
||||
liftError CRYPTO $ C.encrypt encKey paddedSize 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
|
||||
|
||||
encryptUnsigned :: AgentMonad m => SMPClient -> EncryptionKey -> ByteString -> m ByteString
|
||||
encryptUnsigned smp encryptKey msg = do
|
||||
paddedSize <- asks $ (blockSize smp -) . reservedMsgSize
|
||||
size <- asks $ rsaKeySize . config
|
||||
liftError cryptoError $ do
|
||||
enc <- C.encrypt encryptKey paddedSize msg
|
||||
let sig = B.replicate size ' '
|
||||
pure $ sig <> enc
|
||||
|
||||
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
|
||||
|
||||
@@ -1,43 +1,84 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Env.SQLite where
|
||||
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Network.Socket
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.Protocol (SMPServer)
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client
|
||||
import System.Random (StdGen, newStdGen)
|
||||
import UnliftIO.STM
|
||||
|
||||
data AgentConfig = AgentConfig
|
||||
{ tcpPort :: ServiceName,
|
||||
smpServers :: NonEmpty SMPServer,
|
||||
rsaKeySize :: Int,
|
||||
connIdBytes :: Int,
|
||||
tbqSize :: Natural,
|
||||
dbFile :: FilePath,
|
||||
smpCfg :: SMPClientConfig
|
||||
dbPoolSize :: Int,
|
||||
smpCfg :: SMPClientConfig,
|
||||
retryInterval :: RetryInterval,
|
||||
reconnectInterval :: RetryInterval
|
||||
}
|
||||
|
||||
minute :: Int
|
||||
minute = 60_000_000
|
||||
|
||||
defaultAgentConfig :: AgentConfig
|
||||
defaultAgentConfig =
|
||||
AgentConfig
|
||||
{ tcpPort = "5224",
|
||||
smpServers = undefined,
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = "smp-agent.db",
|
||||
dbPoolSize = 4,
|
||||
smpCfg = smpDefaultConfig,
|
||||
retryInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 1_000_000,
|
||||
increaseAfter = minute,
|
||||
maxInterval = 10 * minute
|
||||
},
|
||||
reconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 1_000_000,
|
||||
increaseAfter = 10_000_000,
|
||||
maxInterval = 10_000_000
|
||||
}
|
||||
}
|
||||
|
||||
data Env = Env
|
||||
{ config :: AgentConfig,
|
||||
store :: SQLiteStore,
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
clientCounter :: TVar Int,
|
||||
paddedMsgSize :: Int
|
||||
reservedMsgSize :: Int,
|
||||
randomServer :: TVar StdGen
|
||||
}
|
||||
|
||||
newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env
|
||||
newSMPAgentEnv config = do
|
||||
idsDrg <- drgNew >>= newTVarIO
|
||||
_ <- createSQLiteStore $ dbFile config
|
||||
newSMPAgentEnv cfg = do
|
||||
idsDrg <- newTVarIO =<< drgNew
|
||||
store <- liftIO $ createSQLiteStore (dbFile cfg) (dbPoolSize cfg) Migrations.app
|
||||
clientCounter <- newTVarIO 0
|
||||
return Env {config, idsDrg, clientCounter, paddedMsgSize}
|
||||
randomServer <- newTVarIO =<< liftIO newStdGen
|
||||
return Env {config = cfg, store, idsDrg, clientCounter, reservedMsgSize, randomServer}
|
||||
where
|
||||
-- one rsaKeySize is used by the RSA signature in each command,
|
||||
-- another - by encrypted message body header
|
||||
-- 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
|
||||
paddedMsgSize = blockSize smp - 2 * rsaKeySize config - smpCommandSize smp
|
||||
smp = smpCfg config
|
||||
reservedMsgSize = 3 * rsaKeySize cfg + smpCommandSize (smpCfg cfg)
|
||||
|
||||
@@ -0,0 +1,862 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PolyKinds #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Agent.Protocol
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- Types, parsers, serializers and functions to send and receive SMP agent protocol commands and responses.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md
|
||||
module Simplex.Messaging.Agent.Protocol
|
||||
( -- * SMP agent protocol types
|
||||
ConnInfo,
|
||||
ACommand (..),
|
||||
AParty (..),
|
||||
SAParty (..),
|
||||
MsgHash,
|
||||
MsgMeta (..),
|
||||
SMPMessage (..),
|
||||
AMessage (..),
|
||||
SMPServer (..),
|
||||
SMPQueueUri (..),
|
||||
ConnectionMode (..),
|
||||
SConnectionMode (..),
|
||||
AConnectionMode (..),
|
||||
cmInvitation,
|
||||
cmContact,
|
||||
ConnectionModeI (..),
|
||||
ConnectionRequest (..),
|
||||
AConnectionRequest (..),
|
||||
ConnReqData (..),
|
||||
ConnReqScheme (..),
|
||||
simplexChat,
|
||||
AgentErrorType (..),
|
||||
CommandErrorType (..),
|
||||
ConnectionErrorType (..),
|
||||
BrokerErrorType (..),
|
||||
SMPAgentError (..),
|
||||
ATransmission,
|
||||
ATransmissionOrError,
|
||||
ARawTransmission,
|
||||
ConnId,
|
||||
ConfirmationId,
|
||||
InvitationId,
|
||||
AckMode (..),
|
||||
OnOff (..),
|
||||
MsgIntegrity (..),
|
||||
MsgErrorType (..),
|
||||
QueueStatus (..),
|
||||
SignatureKey,
|
||||
VerificationKey,
|
||||
EncryptionKey,
|
||||
DecryptionKey,
|
||||
ACorrId,
|
||||
AgentMsgId,
|
||||
|
||||
-- * Parse and serialize
|
||||
serializeCommand,
|
||||
serializeSMPMessage,
|
||||
serializeMsgIntegrity,
|
||||
serializeServer,
|
||||
serializeSMPQueueUri,
|
||||
reservedServerKey, -- TODO remove
|
||||
serializeConnMode,
|
||||
serializeConnMode',
|
||||
connMode,
|
||||
connMode',
|
||||
serializeConnReq,
|
||||
serializeConnReq',
|
||||
serializeAgentError,
|
||||
commandP,
|
||||
parseSMPMessage,
|
||||
smpServerP,
|
||||
smpQueueUriP,
|
||||
connModeT,
|
||||
connReqP,
|
||||
connReqP',
|
||||
msgIntegrityP,
|
||||
agentErrorTypeP,
|
||||
agentMessageP,
|
||||
|
||||
-- * TCP transport functions
|
||||
tPut,
|
||||
tGet,
|
||||
tPutRaw,
|
||||
tGetRaw,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Monad.IO.Class
|
||||
import qualified Crypto.PubKey.RSA as R
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Base64
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
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.List (find)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (isJust)
|
||||
import Data.String (IsString (..))
|
||||
import Data.Text (Text)
|
||||
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.HTTP.Types (parseSimpleQuery, renderSimpleQuery)
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol
|
||||
( ErrorType,
|
||||
MsgBody,
|
||||
MsgId,
|
||||
SenderPublicKey,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP)
|
||||
import Simplex.Messaging.Util
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
import Text.Read
|
||||
import UnliftIO.Exception (Exception)
|
||||
|
||||
-- | Raw (unparsed) SMP agent protocol transmission.
|
||||
type ARawTransmission = (ByteString, ByteString, ByteString)
|
||||
|
||||
-- | Parsed SMP agent protocol transmission.
|
||||
type ATransmission p = (ACorrId, ConnId, ACommand p)
|
||||
|
||||
-- | SMP agent protocol transmission or transmission error.
|
||||
type ATransmissionOrError p = (ACorrId, ConnId, Either AgentErrorType (ACommand p))
|
||||
|
||||
type ACorrId = ByteString
|
||||
|
||||
-- | SMP agent protocol participants.
|
||||
data AParty = Agent | Client
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Singleton types for SMP agent protocol participants.
|
||||
data SAParty :: AParty -> Type where
|
||||
SAgent :: SAParty Agent
|
||||
SClient :: SAParty Client
|
||||
|
||||
deriving instance Show (SAParty p)
|
||||
|
||||
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
|
||||
|
||||
type ConnInfo = ByteString
|
||||
|
||||
-- | Parameterized type for SMP agent protocol commands and responses from all participants.
|
||||
data ACommand (p :: AParty) where
|
||||
NEW :: AConnectionMode -> ACommand Client -- response INV
|
||||
INV :: AConnectionRequest -> ACommand Agent
|
||||
JOIN :: AConnectionRequest -> ConnInfo -> ACommand Client -- response OK
|
||||
CONF :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender
|
||||
LET :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client
|
||||
REQ :: InvitationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender
|
||||
ACPT :: InvitationId -> ConnInfo -> ACommand Client -- ConnInfo is from client
|
||||
RJCT :: InvitationId -> ACommand Client
|
||||
INFO :: ConnInfo -> ACommand Agent
|
||||
CON :: ACommand Agent -- notification that connection is established
|
||||
SUB :: ACommand Client
|
||||
END :: ACommand Agent
|
||||
DOWN :: ACommand Agent
|
||||
UP :: ACommand Agent
|
||||
-- QST :: QueueDirection -> ACommand Client
|
||||
-- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent
|
||||
SEND :: MsgBody -> ACommand Client
|
||||
MID :: AgentMsgId -> ACommand Agent
|
||||
SENT :: AgentMsgId -> ACommand Agent
|
||||
MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent
|
||||
MSG :: MsgMeta -> 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)
|
||||
|
||||
data ConnectionMode = CMInvitation | CMContact
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SConnectionMode (m :: ConnectionMode) where
|
||||
SCMInvitation :: SConnectionMode CMInvitation
|
||||
SCMContact :: SConnectionMode CMContact
|
||||
|
||||
deriving instance Eq (SConnectionMode m)
|
||||
|
||||
deriving instance Show (SConnectionMode m)
|
||||
|
||||
instance TestEquality SConnectionMode where
|
||||
testEquality SCMInvitation SCMInvitation = Just Refl
|
||||
testEquality SCMContact SCMContact = Just Refl
|
||||
testEquality _ _ = Nothing
|
||||
|
||||
data AConnectionMode = forall m. ACM (SConnectionMode m)
|
||||
|
||||
instance Eq AConnectionMode where
|
||||
ACM m == ACM m' = isJust $ testEquality m m'
|
||||
|
||||
cmInvitation :: AConnectionMode
|
||||
cmInvitation = ACM SCMInvitation
|
||||
|
||||
cmContact :: AConnectionMode
|
||||
cmContact = ACM SCMContact
|
||||
|
||||
deriving instance Show AConnectionMode
|
||||
|
||||
connMode :: SConnectionMode m -> ConnectionMode
|
||||
connMode SCMInvitation = CMInvitation
|
||||
connMode SCMContact = CMContact
|
||||
|
||||
connMode' :: ConnectionMode -> AConnectionMode
|
||||
connMode' CMInvitation = cmInvitation
|
||||
connMode' CMContact = cmContact
|
||||
|
||||
class ConnectionModeI (m :: ConnectionMode) where sConnectionMode :: SConnectionMode m
|
||||
|
||||
instance ConnectionModeI CMInvitation where sConnectionMode = SCMInvitation
|
||||
|
||||
instance ConnectionModeI CMContact where sConnectionMode = SCMContact
|
||||
|
||||
type MsgHash = ByteString
|
||||
|
||||
-- | Agent message metadata sent to the client
|
||||
data MsgMeta = MsgMeta
|
||||
{ integrity :: MsgIntegrity,
|
||||
recipient :: (AgentMsgId, UTCTime),
|
||||
broker :: (MsgId, UTCTime),
|
||||
sender :: (AgentMsgId, UTCTime)
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | SMP message formats.
|
||||
data SMPMessage
|
||||
= -- | SMP confirmation
|
||||
-- (see <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#send-message SMP protocol>)
|
||||
SMPConfirmation
|
||||
{ -- | sender's public key to use for authentication of sender's commands at the recepient's server
|
||||
senderKey :: SenderPublicKey,
|
||||
-- | sender's information to be associated with the connection, e.g. sender's profile information
|
||||
connInfo :: ConnInfo
|
||||
}
|
||||
| -- | Agent message header and envelope for client messages
|
||||
-- (see <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#messages-between-smp-agents SMP agent protocol>)
|
||||
SMPMessage
|
||||
{ -- | sequential ID assigned by the sending agent
|
||||
senderMsgId :: AgentMsgId,
|
||||
-- | timestamp from the sending agent
|
||||
senderTimestamp :: SenderTimestamp,
|
||||
-- | digest of the previous message
|
||||
previousMsgHash :: MsgHash,
|
||||
-- | messages sent between agents once queue is secured
|
||||
agentMessage :: AMessage
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- | Messages sent between SMP agents once SMP queue is secured.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#messages-between-smp-agents
|
||||
data AMessage where
|
||||
-- | the first message in the queue to validate it is secured
|
||||
HELLO :: VerificationKey -> AckMode -> AMessage
|
||||
-- | reply queue information
|
||||
REPLY :: ConnectionRequest CMInvitation -> AMessage
|
||||
-- | agent envelope for the client message
|
||||
A_MSG :: MsgBody -> AMessage
|
||||
-- | connection request with the invitation to connect
|
||||
A_INV :: ConnectionRequest CMInvitation -> ConnInfo -> AMessage
|
||||
deriving (Show)
|
||||
|
||||
-- | Parse SMP message.
|
||||
parseSMPMessage :: ByteString -> Either AgentErrorType SMPMessage
|
||||
parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE
|
||||
where
|
||||
smpMessageP :: Parser SMPMessage
|
||||
smpMessageP = A.endOfLine *> smpClientMessageP <|> smpConfirmationP
|
||||
|
||||
smpConfirmationP :: Parser SMPMessage
|
||||
smpConfirmationP = "KEY " *> (SMPConfirmation <$> C.pubKeyP <* A.endOfLine <* A.endOfLine <*> binaryBodyP <* 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
|
||||
|
||||
-- | Serialize SMP message.
|
||||
serializeSMPMessage :: SMPMessage -> ByteString
|
||||
serializeSMPMessage = \case
|
||||
SMPConfirmation sKey cInfo -> smpMessage ("KEY " <> C.serializePubKey sKey) "" (serializeBinary cInfo) <> "\n"
|
||||
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
|
||||
<|> "INV " *> a_inv
|
||||
where
|
||||
hello = HELLO <$> C.pubKeyP <*> ackMode
|
||||
reply = REPLY <$> connReqP'
|
||||
a_msg = A_MSG <$> binaryBodyP <* A.endOfLine
|
||||
a_inv = A_INV <$> connReqP' <* A.space <*> binaryBodyP <* A.endOfLine
|
||||
ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On)
|
||||
|
||||
-- | SMP server location parser.
|
||||
smpServerP :: Parser SMPServer
|
||||
smpServerP = SMPServer <$> server <*> optional port <*> optional kHash
|
||||
where
|
||||
server = B.unpack <$> A.takeWhile1 (A.notInClass ":#,; ")
|
||||
port = A.char ':' *> (B.unpack <$> A.takeWhile1 A.isDigit)
|
||||
kHash = C.KeyHash <$> (A.char '#' *> base64P)
|
||||
|
||||
serializeAgentMessage :: AMessage -> ByteString
|
||||
serializeAgentMessage = \case
|
||||
HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else ""
|
||||
REPLY cReq -> "REPLY " <> serializeConnReq' cReq
|
||||
A_MSG body -> "MSG " <> serializeBinary body <> "\n"
|
||||
A_INV cReq cInfo -> B.unwords ["INV", serializeConnReq' cReq, serializeBinary cInfo] <> "\n"
|
||||
|
||||
-- | Serialize SMP queue information that is sent out-of-band.
|
||||
serializeSMPQueueUri :: SMPQueueUri -> ByteString
|
||||
serializeSMPQueueUri (SMPQueueUri srv qId _) =
|
||||
serializeServerUri srv <> "/" <> U.encode qId <> "#"
|
||||
|
||||
-- | SMP queue information parser.
|
||||
smpQueueUriP :: Parser SMPQueueUri
|
||||
smpQueueUriP =
|
||||
SMPQueueUri <$> smpServerUriP <* "/" <*> base64UriP <* "#" <*> pure reservedServerKey
|
||||
|
||||
reservedServerKey :: C.PublicKey
|
||||
reservedServerKey = C.PublicKey $ R.PublicKey 1 0 0
|
||||
|
||||
serializeConnReq :: AConnectionRequest -> ByteString
|
||||
serializeConnReq (ACR _ cr) = serializeConnReq' cr
|
||||
|
||||
serializeConnReq' :: ConnectionRequest m -> ByteString
|
||||
serializeConnReq' = \case
|
||||
CRInvitation crData -> serialize CMInvitation crData
|
||||
CRContact crData -> serialize CMContact crData
|
||||
where
|
||||
serialize crMode ConnReqData {crScheme, crSmpQueues, crEncryptKey} =
|
||||
sch <> "/" <> m <> "#/" <> queryStr
|
||||
where
|
||||
sch = case crScheme of
|
||||
CRSSimplex -> "simplex:"
|
||||
CRSAppServer host port -> B.pack $ "https://" <> host <> maybe "" (':' :) port
|
||||
m = case crMode of
|
||||
CMInvitation -> "invitation"
|
||||
CMContact -> "contact"
|
||||
queryStr = renderSimpleQuery True [("smp", queues), ("e2e", key)]
|
||||
queues = B.intercalate "," . map serializeSMPQueueUri $ L.toList crSmpQueues
|
||||
key = C.serializePubKeyUri crEncryptKey
|
||||
|
||||
connReqP' :: forall m. ConnectionModeI m => Parser (ConnectionRequest m)
|
||||
connReqP' = do
|
||||
ACR m cr <- connReqP
|
||||
case testEquality m $ sConnectionMode @m of
|
||||
Just Refl -> pure cr
|
||||
_ -> fail "bad connection request mode"
|
||||
|
||||
connReqP :: Parser AConnectionRequest
|
||||
connReqP = do
|
||||
crScheme <- "simplex:" $> CRSSimplex <|> "https://" *> appServer
|
||||
crMode <- "/" *> mode <* "#/?"
|
||||
query <- parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n')
|
||||
crSmpQueues <- paramP "smp" smpQueues query
|
||||
crEncryptKey <- paramP "e2e" C.pubKeyUriP query
|
||||
let cReq = ConnReqData {crScheme, crSmpQueues, crEncryptKey}
|
||||
pure $ case crMode of
|
||||
CMInvitation -> ACR SCMInvitation $ CRInvitation cReq
|
||||
CMContact -> ACR SCMContact $ CRContact cReq
|
||||
where
|
||||
appServer = CRSAppServer <$> host <*> optional port
|
||||
host = B.unpack <$> A.takeTill (\c -> c == ':' || c == '/')
|
||||
port = B.unpack <$> (A.char ':' *> A.takeTill (== '/'))
|
||||
mode = "invitation" $> CMInvitation <|> "contact" $> CMContact
|
||||
paramP param parser query =
|
||||
let p = maybe (fail "") (pure . snd) $ find ((== param) . fst) query
|
||||
in parseAll parser <$?> p
|
||||
smpQueues =
|
||||
maybe (fail "no SMP queues") pure . L.nonEmpty
|
||||
=<< (smpQueue `A.sepBy1'` A.char ',')
|
||||
smpQueue = parseAll smpQueueUriP <$?> A.takeTill (== ',')
|
||||
|
||||
-- | Serialize SMP server location.
|
||||
serializeServer :: SMPServer -> ByteString
|
||||
serializeServer SMPServer {host, port, keyHash} =
|
||||
B.pack $ host <> maybe "" (':' :) port <> maybe "" (('#' :) . B.unpack . encode . C.unKeyHash) keyHash
|
||||
|
||||
serializeServerUri :: SMPServer -> ByteString
|
||||
serializeServerUri SMPServer {host, port, keyHash} = "smp://" <> kh <> B.pack host <> p
|
||||
where
|
||||
kh = maybe "" ((<> "@") . U.encode . C.unKeyHash) keyHash
|
||||
p = B.pack $ maybe "" (':' :) port
|
||||
|
||||
smpServerUriP :: Parser SMPServer
|
||||
smpServerUriP = do
|
||||
_ <- "smp://"
|
||||
keyHash <- optional $ C.KeyHash <$> (U.decode <$?> A.takeTill (== '@') <* A.char '@')
|
||||
host <- B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ")
|
||||
port <- optional $ B.unpack <$> (A.char ':' *> A.takeWhile1 A.isDigit)
|
||||
pure SMPServer {host, port, keyHash}
|
||||
|
||||
serializeConnMode :: AConnectionMode -> ByteString
|
||||
serializeConnMode (ACM cMode) = serializeConnMode' $ connMode cMode
|
||||
|
||||
serializeConnMode' :: ConnectionMode -> ByteString
|
||||
serializeConnMode' = \case
|
||||
CMInvitation -> "INV"
|
||||
CMContact -> "CON"
|
||||
|
||||
connModeP' :: Parser ConnectionMode
|
||||
connModeP' = "INV" $> CMInvitation <|> "CON" $> CMContact
|
||||
|
||||
connModeP :: Parser AConnectionMode
|
||||
connModeP = connMode' <$> connModeP'
|
||||
|
||||
connModeT :: Text -> Maybe ConnectionMode
|
||||
connModeT = \case
|
||||
"INV" -> Just CMInvitation
|
||||
"CON" -> Just CMContact
|
||||
_ -> Nothing
|
||||
|
||||
-- | SMP server location and transport key digest (hash).
|
||||
data SMPServer = SMPServer
|
||||
{ host :: HostName,
|
||||
port :: Maybe ServiceName,
|
||||
keyHash :: Maybe C.KeyHash
|
||||
}
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
instance IsString SMPServer where
|
||||
fromString = parseString $ parseAll smpServerP
|
||||
|
||||
-- | SMP agent connection alias.
|
||||
type ConnId = ByteString
|
||||
|
||||
type ConfirmationId = ByteString
|
||||
|
||||
type InvitationId = ByteString
|
||||
|
||||
-- | Connection modes.
|
||||
data OnOff = On | Off deriving (Eq, Show, Read)
|
||||
|
||||
-- | Message acknowledgement mode of the connection.
|
||||
newtype AckMode = AckMode OnOff deriving (Eq, Show)
|
||||
|
||||
-- | SMP queue information sent out-of-band.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#out-of-band-messages
|
||||
data SMPQueueUri = SMPQueueUri
|
||||
{ smpServer :: SMPServer,
|
||||
senderId :: SMP.SenderId,
|
||||
serverVerifyKey :: VerificationKey
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ConnectionRequest (m :: ConnectionMode) where
|
||||
CRInvitation :: ConnReqData -> ConnectionRequest CMInvitation
|
||||
CRContact :: ConnReqData -> ConnectionRequest CMContact
|
||||
|
||||
deriving instance Eq (ConnectionRequest m)
|
||||
|
||||
deriving instance Show (ConnectionRequest m)
|
||||
|
||||
data AConnectionRequest = forall m. ACR (SConnectionMode m) (ConnectionRequest m)
|
||||
|
||||
instance Eq AConnectionRequest where
|
||||
ACR m cr == ACR m' cr' = case testEquality m m' of
|
||||
Just Refl -> cr == cr'
|
||||
_ -> False
|
||||
|
||||
deriving instance Show AConnectionRequest
|
||||
|
||||
data ConnReqData = ConnReqData
|
||||
{ crScheme :: ConnReqScheme,
|
||||
crSmpQueues :: L.NonEmpty SMPQueueUri,
|
||||
crEncryptKey :: EncryptionKey
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ConnReqScheme = CRSSimplex | CRSAppServer HostName (Maybe ServiceName)
|
||||
deriving (Eq, Show)
|
||||
|
||||
simplexChat :: ConnReqScheme
|
||||
simplexChat = CRSAppServer "simplex.chat" Nothing
|
||||
|
||||
-- | Public key used to E2E encrypt SMP messages.
|
||||
type EncryptionKey = C.PublicKey
|
||||
|
||||
-- | Private key used to E2E decrypt SMP messages.
|
||||
type DecryptionKey = C.SafePrivateKey
|
||||
|
||||
-- | Private key used to sign SMP commands
|
||||
type SignatureKey = C.APrivateKey
|
||||
|
||||
-- | Public key used by SMP server to authorize (verify) SMP commands.
|
||||
type VerificationKey = C.PublicKey
|
||||
|
||||
data QueueDirection = SND | RCV deriving (Show)
|
||||
|
||||
-- | SMP queue status.
|
||||
data QueueStatus
|
||||
= -- | queue is created
|
||||
New
|
||||
| -- | queue is confirmed by the sender
|
||||
Confirmed
|
||||
| -- | queue is secured with sender key (only used by the queue recipient)
|
||||
Secured
|
||||
| -- | queue is active
|
||||
Active
|
||||
| -- | queue is disabled (only used by the queue recipient)
|
||||
Disabled
|
||||
deriving (Eq, Show, Read)
|
||||
|
||||
type AgentMsgId = Int64
|
||||
|
||||
type SenderTimestamp = UTCTime
|
||||
|
||||
-- | Result of received message integrity validation.
|
||||
data MsgIntegrity = MsgOk | MsgError MsgErrorType
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Error of message integrity validation.
|
||||
data MsgErrorType = MsgSkipped AgentMsgId AgentMsgId | MsgBadId AgentMsgId | MsgBadHash | MsgDuplicate
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Error type used in errors sent to agent clients.
|
||||
data AgentErrorType
|
||||
= -- | command or response error
|
||||
CMD CommandErrorType
|
||||
| -- | connection errors
|
||||
CONN ConnectionErrorType
|
||||
| -- | SMP protocol errors forwarded to agent clients
|
||||
SMP ErrorType
|
||||
| -- | SMP server errors
|
||||
BROKER BrokerErrorType
|
||||
| -- | errors of other agents
|
||||
AGENT SMPAgentError
|
||||
| -- | agent implementation or dependency errors
|
||||
INTERNAL String
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
-- | SMP agent protocol command or response error.
|
||||
data CommandErrorType
|
||||
= -- | command is prohibited in this context
|
||||
PROHIBITED
|
||||
| -- | command syntax is invalid
|
||||
SYNTAX
|
||||
| -- | entity ID is required with this command
|
||||
NO_CONN
|
||||
| -- | message size is not correct (no terminating space)
|
||||
SIZE
|
||||
| -- | message does not fit in SMP block
|
||||
LARGE
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
-- | Connection error.
|
||||
data ConnectionErrorType
|
||||
= -- | connection alias is not in the database
|
||||
NOT_FOUND
|
||||
| -- | connection alias already exists
|
||||
DUPLICATE
|
||||
| -- | connection is simplex, but operation requires another queue
|
||||
SIMPLEX
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
-- | SMP server errors.
|
||||
data BrokerErrorType
|
||||
= -- | invalid server response (failed to parse)
|
||||
RESPONSE ErrorType
|
||||
| -- | unexpected response
|
||||
UNEXPECTED
|
||||
| -- | network error
|
||||
NETWORK
|
||||
| -- | handshake or other transport error
|
||||
TRANSPORT TransportError
|
||||
| -- | command response timeout
|
||||
TIMEOUT
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
-- | Errors of another SMP agent.
|
||||
data SMPAgentError
|
||||
= -- | possibly should include bytestring that failed to parse
|
||||
A_MESSAGE
|
||||
| -- | possibly should include the prohibited SMP/agent message
|
||||
A_PROHIBITED
|
||||
| -- | cannot RSA/AES-decrypt or parse decrypted header
|
||||
A_ENCRYPTION
|
||||
| -- | invalid RSA signature
|
||||
A_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
|
||||
|
||||
-- | SMP agent command and response parser
|
||||
commandP :: Parser ACmd
|
||||
commandP =
|
||||
"NEW " *> newCmd
|
||||
<|> "INV " *> invResp
|
||||
<|> "JOIN " *> joinCmd
|
||||
<|> "CONF " *> confMsg
|
||||
<|> "LET " *> letCmd
|
||||
<|> "REQ " *> reqMsg
|
||||
<|> "ACPT " *> acptCmd
|
||||
<|> "RJCT " *> rjctCmd
|
||||
<|> "INFO " *> infoCmd
|
||||
<|> "SUB" $> ACmd SClient SUB
|
||||
<|> "END" $> ACmd SAgent END
|
||||
<|> "DOWN" $> ACmd SAgent DOWN
|
||||
<|> "UP" $> ACmd SAgent UP
|
||||
<|> "SEND " *> sendCmd
|
||||
<|> "MID " *> msgIdResp
|
||||
<|> "SENT " *> sentResp
|
||||
<|> "MERR " *> msgErrResp
|
||||
<|> "MSG " *> message
|
||||
<|> "ACK " *> ackCmd
|
||||
<|> "OFF" $> ACmd SClient OFF
|
||||
<|> "DEL" $> ACmd SClient DEL
|
||||
<|> "ERR " *> agentError
|
||||
<|> "CON" $> ACmd SAgent CON
|
||||
<|> "OK" $> ACmd SAgent OK
|
||||
where
|
||||
newCmd = ACmd SClient . NEW <$> connModeP
|
||||
invResp = ACmd SAgent . INV <$> connReqP
|
||||
joinCmd = ACmd SClient <$> (JOIN <$> connReqP <* A.space <*> A.takeByteString)
|
||||
confMsg = ACmd SAgent <$> (CONF <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
|
||||
letCmd = ACmd SClient <$> (LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
|
||||
reqMsg = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
|
||||
acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
|
||||
rjctCmd = ACmd SClient . RJCT <$> A.takeByteString
|
||||
infoCmd = ACmd SAgent . INFO <$> A.takeByteString
|
||||
sendCmd = ACmd SClient . SEND <$> A.takeByteString
|
||||
msgIdResp = ACmd SAgent . MID <$> A.decimal
|
||||
sentResp = ACmd SAgent . SENT <$> A.decimal
|
||||
msgErrResp = ACmd SAgent <$> (MERR <$> A.decimal <* A.space <*> agentErrorTypeP)
|
||||
message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString)
|
||||
ackCmd = ACmd SClient . ACK <$> A.decimal
|
||||
msgMetaP = do
|
||||
integrity <- msgIntegrityP
|
||||
recipient <- " R=" *> partyMeta A.decimal
|
||||
broker <- " B=" *> partyMeta base64P
|
||||
sender <- " S=" *> partyMeta A.decimal
|
||||
pure MsgMeta {integrity, recipient, broker, sender}
|
||||
partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P
|
||||
agentError = ACmd SAgent . ERR <$> agentErrorTypeP
|
||||
|
||||
-- | Message integrity validation result parser.
|
||||
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
|
||||
|
||||
-- | Serialize SMP agent command.
|
||||
serializeCommand :: ACommand p -> ByteString
|
||||
serializeCommand = \case
|
||||
NEW cMode -> "NEW " <> serializeConnMode cMode
|
||||
INV cReq -> "INV " <> serializeConnReq cReq
|
||||
JOIN cReq cInfo -> B.unwords ["JOIN", serializeConnReq cReq, serializeBinary cInfo]
|
||||
CONF confId cInfo -> B.unwords ["CONF", confId, serializeBinary cInfo]
|
||||
LET confId cInfo -> B.unwords ["LET", confId, serializeBinary cInfo]
|
||||
REQ invId cInfo -> B.unwords ["REQ", invId, serializeBinary cInfo]
|
||||
ACPT invId cInfo -> B.unwords ["ACPT", invId, serializeBinary cInfo]
|
||||
RJCT invId -> "RJCT " <> invId
|
||||
INFO cInfo -> "INFO " <> serializeBinary cInfo
|
||||
SUB -> "SUB"
|
||||
END -> "END"
|
||||
DOWN -> "DOWN"
|
||||
UP -> "UP"
|
||||
SEND msgBody -> "SEND " <> serializeBinary msgBody
|
||||
MID mId -> "MID " <> bshow mId
|
||||
SENT mId -> "SENT " <> bshow mId
|
||||
MERR mId e -> B.unwords ["MERR", bshow mId, serializeAgentError e]
|
||||
MSG msgMeta msgBody -> B.unwords ["MSG", serializeMsgMeta msgMeta, serializeBinary msgBody]
|
||||
ACK mId -> "ACK " <> bshow mId
|
||||
OFF -> "OFF"
|
||||
DEL -> "DEL"
|
||||
CON -> "CON"
|
||||
ERR e -> "ERR " <> serializeAgentError e
|
||||
OK -> "OK"
|
||||
where
|
||||
showTs :: UTCTime -> ByteString
|
||||
showTs = B.pack . formatISO8601Millis
|
||||
serializeMsgMeta :: MsgMeta -> ByteString
|
||||
serializeMsgMeta MsgMeta {integrity, recipient = (rmId, rTs), broker = (bmId, bTs), sender = (smId, sTs)} =
|
||||
B.unwords
|
||||
[ serializeMsgIntegrity integrity,
|
||||
"R=" <> bshow rmId <> "," <> showTs rTs,
|
||||
"B=" <> encode bmId <> "," <> showTs bTs,
|
||||
"S=" <> bshow smId <> "," <> showTs sTs
|
||||
]
|
||||
|
||||
-- | Serialize message integrity validation result.
|
||||
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"
|
||||
|
||||
-- | SMP agent protocol error parser.
|
||||
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
|
||||
|
||||
-- | Serialize SMP agent protocol error.
|
||||
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
|
||||
|
||||
binaryBodyP :: Parser ByteString
|
||||
binaryBodyP = do
|
||||
size :: Int <- A.decimal <* A.endOfLine
|
||||
A.take size
|
||||
|
||||
serializeBinary :: ByteString -> ByteString
|
||||
serializeBinary body = bshow (B.length body) <> "\n" <> body
|
||||
|
||||
-- | Send raw (unparsed) SMP agent protocol transmission to TCP connection.
|
||||
tPutRaw :: Transport c => c -> ARawTransmission -> IO ()
|
||||
tPutRaw h (corrId, entity, command) = do
|
||||
putLn h corrId
|
||||
putLn h entity
|
||||
putLn h command
|
||||
|
||||
-- | Receive raw (unparsed) SMP agent protocol transmission from TCP connection.
|
||||
tGetRaw :: Transport c => c -> IO ARawTransmission
|
||||
tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h
|
||||
|
||||
-- | Send SMP agent protocol command (or response) to TCP connection.
|
||||
tPut :: (Transport c, MonadIO m) => c -> ATransmission p -> m ()
|
||||
tPut h (corrId, connAlias, command) =
|
||||
liftIO $ tPutRaw h (corrId, connAlias, serializeCommand command)
|
||||
|
||||
-- | Receive client and agent transmissions from TCP connection.
|
||||
tGet :: forall c m p. (Transport c, MonadIO m) => SAParty p -> c -> m (ATransmissionOrError p)
|
||||
tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
where
|
||||
tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p)
|
||||
tParseLoadBody t@(corrId, connId, command) = do
|
||||
let cmd = parseCommand command >>= fromParty >>= tConnId t
|
||||
fullCmd <- either (return . Left) cmdWithMsgBody cmd
|
||||
return (corrId, connId, fullCmd)
|
||||
|
||||
fromParty :: ACmd -> Either AgentErrorType (ACommand p)
|
||||
fromParty (ACmd (p :: p1) cmd) = case testEquality party p of
|
||||
Just Refl -> Right cmd
|
||||
_ -> Left $ CMD PROHIBITED
|
||||
|
||||
tConnId :: ARawTransmission -> ACommand p -> Either AgentErrorType (ACommand p)
|
||||
tConnId (_, connId, _) cmd = case cmd of
|
||||
-- NEW, JOIN and ACPT have optional connId
|
||||
NEW _ -> Right cmd
|
||||
JOIN {} -> Right cmd
|
||||
ACPT {} -> Right cmd
|
||||
-- ERROR response does not always have connId
|
||||
ERR _ -> Right cmd
|
||||
-- other responses must have connId
|
||||
_
|
||||
| B.null connId -> Left $ CMD NO_CONN
|
||||
| otherwise -> Right cmd
|
||||
|
||||
cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p))
|
||||
cmdWithMsgBody = \case
|
||||
SEND body -> SEND <$$> getBody body
|
||||
MSG msgMeta body -> MSG msgMeta <$$> getBody body
|
||||
JOIN qUri cInfo -> JOIN qUri <$$> getBody cInfo
|
||||
CONF confId cInfo -> CONF confId <$$> getBody cInfo
|
||||
LET confId cInfo -> LET confId <$$> getBody cInfo
|
||||
REQ invId cInfo -> REQ invId <$$> getBody cInfo
|
||||
ACPT invId cInfo -> ACPT invId <$$> getBody cInfo
|
||||
INFO cInfo -> INFO <$$> getBody cInfo
|
||||
cmd -> pure $ Right cmd
|
||||
|
||||
-- TODO refactor with server
|
||||
getBody :: ByteString -> m (Either AgentErrorType ByteString)
|
||||
getBody binary =
|
||||
case B.unpack binary of
|
||||
':' : body -> return . Right $ B.pack body
|
||||
str -> case readMaybe str :: Maybe Int of
|
||||
Just size -> liftIO $ do
|
||||
body <- cGet 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,28 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.RetryInterval where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||
|
||||
data RetryInterval = RetryInterval
|
||||
{ initialInterval :: Int,
|
||||
increaseAfter :: Int,
|
||||
maxInterval :: Int
|
||||
}
|
||||
|
||||
withRetryInterval :: forall m. MonadIO m => RetryInterval -> (m () -> m ()) -> m ()
|
||||
withRetryInterval RetryInterval {initialInterval, increaseAfter, maxInterval} action =
|
||||
callAction 0 initialInterval
|
||||
where
|
||||
callAction :: Int -> Int -> m ()
|
||||
callAction elapsedTime delay = action loop
|
||||
where
|
||||
loop = do
|
||||
let newDelay =
|
||||
if elapsedTime < increaseAfter || delay == maxInterval
|
||||
then delay
|
||||
else min (delay * 3 `div` 2) maxInterval
|
||||
liftIO $ threadDelay delay
|
||||
callAction (elapsedTime + delay) newDelay
|
||||
@@ -9,12 +9,15 @@
|
||||
|
||||
module Simplex.Messaging.Agent.Store where
|
||||
|
||||
import Control.Concurrent.STM (TVar)
|
||||
import Control.Exception (Exception)
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Type.Equality
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Protocol
|
||||
( MsgBody,
|
||||
MsgId,
|
||||
@@ -29,21 +32,42 @@ import qualified Simplex.Messaging.Protocol as SMP
|
||||
-- | 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 ()
|
||||
createRcvConn :: s -> TVar ChaChaDRG -> ConnData -> RcvQueue -> SConnectionMode c -> m ConnId
|
||||
createSndConn :: s -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId
|
||||
getConn :: s -> ConnId -> m SomeConn
|
||||
getAllConnIds :: s -> m [ConnId] -- TODO remove - hack for subscribing to all
|
||||
getRcvConn :: s -> SMPServer -> SMP.RecipientId -> m SomeConn
|
||||
deleteConn :: s -> ConnId -> m ()
|
||||
upgradeRcvConnToDuplex :: s -> ConnId -> SndQueue -> m ()
|
||||
upgradeSndConnToDuplex :: s -> ConnId -> RcvQueue -> m ()
|
||||
setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m ()
|
||||
setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m ()
|
||||
setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m ()
|
||||
updateSignKey :: s -> SndQueue -> SignatureKey -> m ()
|
||||
|
||||
-- Confirmations
|
||||
createConfirmation :: s -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId
|
||||
acceptConfirmation :: s -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation
|
||||
getAcceptedConfirmation :: s -> ConnId -> m AcceptedConfirmation
|
||||
removeConfirmations :: s -> ConnId -> m ()
|
||||
|
||||
-- Invitations - sent via Contact connections
|
||||
createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId
|
||||
getInvitation :: s -> InvitationId -> m Invitation
|
||||
acceptInvitation :: s -> InvitationId -> ConnInfo -> m ()
|
||||
deleteInvitation :: s -> ConnId -> InvitationId -> m ()
|
||||
|
||||
-- Msg management
|
||||
createRcvMsg :: s -> ConnAlias -> MsgBody -> InternalTs -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> m InternalId
|
||||
createSndMsg :: s -> ConnAlias -> MsgBody -> InternalTs -> m InternalId
|
||||
getMsg :: s -> ConnAlias -> InternalId -> m Msg
|
||||
updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
|
||||
createRcvMsg :: s -> ConnId -> RcvMsgData -> m ()
|
||||
updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
createSndMsg :: s -> ConnId -> SndMsgData -> m ()
|
||||
updateSndMsgStatus :: s -> ConnId -> InternalId -> SndMsgStatus -> m ()
|
||||
getPendingMsgData :: s -> ConnId -> InternalId -> m MsgBody
|
||||
getPendingMsgs :: s -> ConnId -> m [InternalId]
|
||||
getMsg :: s -> ConnId -> InternalId -> m Msg
|
||||
checkRcvMsg :: s -> ConnId -> InternalId -> m ()
|
||||
updateRcvMsgAck :: s -> ConnId -> InternalId -> m ()
|
||||
|
||||
-- * Queue types
|
||||
|
||||
@@ -51,10 +75,8 @@ class Monad m => MonadAgentStore s m where
|
||||
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
|
||||
@@ -65,7 +87,6 @@ data RcvQueue = RcvQueue
|
||||
data SndQueue = SndQueue
|
||||
{ server :: SMPServer,
|
||||
sndId :: SMP.SenderId,
|
||||
connAlias :: ConnAlias,
|
||||
sndPrivateKey :: SenderPrivateKey,
|
||||
encryptKey :: EncryptionKey,
|
||||
signKey :: SignatureKey,
|
||||
@@ -76,7 +97,7 @@ data SndQueue = SndQueue
|
||||
-- * Connection types
|
||||
|
||||
-- | Type of a connection.
|
||||
data ConnType = CRcv | CSnd | CDuplex deriving (Eq, Show)
|
||||
data ConnType = CRcv | CSnd | CDuplex | CContact deriving (Eq, Show)
|
||||
|
||||
-- | Connection of a specific type.
|
||||
--
|
||||
@@ -89,9 +110,10 @@ data ConnType = CRcv | CSnd | CDuplex deriving (Eq, Show)
|
||||
-- - 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
|
||||
RcvConnection :: ConnData -> RcvQueue -> Connection CRcv
|
||||
SndConnection :: ConnData -> SndQueue -> Connection CSnd
|
||||
DuplexConnection :: ConnData -> RcvQueue -> SndQueue -> Connection CDuplex
|
||||
ContactConnection :: ConnData -> RcvQueue -> Connection CContact
|
||||
|
||||
deriving instance Eq (Connection d)
|
||||
|
||||
@@ -101,6 +123,13 @@ data SConnType :: ConnType -> Type where
|
||||
SCRcv :: SConnType CRcv
|
||||
SCSnd :: SConnType CSnd
|
||||
SCDuplex :: SConnType CDuplex
|
||||
SCContact :: SConnType CContact
|
||||
|
||||
connType :: SConnType c -> ConnType
|
||||
connType SCRcv = CRcv
|
||||
connType SCSnd = CSnd
|
||||
connType SCDuplex = CDuplex
|
||||
connType SCContact = CContact
|
||||
|
||||
deriving instance Eq (SConnType d)
|
||||
|
||||
@@ -110,6 +139,7 @@ instance TestEquality SConnType where
|
||||
testEquality SCRcv SCRcv = Just Refl
|
||||
testEquality SCSnd SCSnd = Just Refl
|
||||
testEquality SCDuplex SCDuplex = Just Refl
|
||||
testEquality SCContact SCContact = Just Refl
|
||||
testEquality _ _ = Nothing
|
||||
|
||||
-- | Connection of an unknown type.
|
||||
@@ -123,6 +153,84 @@ instance Eq SomeConn where
|
||||
|
||||
deriving instance Show SomeConn
|
||||
|
||||
newtype ConnData = ConnData {connId :: ConnId}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- * Confirmation types
|
||||
|
||||
data NewConfirmation = NewConfirmation
|
||||
{ connId :: ConnId,
|
||||
senderKey :: SenderPublicKey,
|
||||
senderConnInfo :: ConnInfo
|
||||
}
|
||||
|
||||
data AcceptedConfirmation = AcceptedConfirmation
|
||||
{ confirmationId :: ConfirmationId,
|
||||
connId :: ConnId,
|
||||
senderKey :: SenderPublicKey,
|
||||
senderConnInfo :: ConnInfo,
|
||||
ownConnInfo :: ConnInfo
|
||||
}
|
||||
|
||||
-- * Invitations
|
||||
|
||||
data NewInvitation = NewInvitation
|
||||
{ contactConnId :: ConnId,
|
||||
connReq :: ConnectionRequest 'CMInvitation,
|
||||
recipientConnInfo :: ConnInfo
|
||||
}
|
||||
|
||||
data Invitation = Invitation
|
||||
{ invitationId :: InvitationId,
|
||||
contactConnId :: ConnId,
|
||||
connReq :: ConnectionRequest 'CMInvitation,
|
||||
recipientConnInfo :: ConnInfo,
|
||||
ownConnInfo :: Maybe ConnInfo,
|
||||
accepted :: Bool
|
||||
}
|
||||
|
||||
-- * Message integrity validation types
|
||||
|
||||
-- | 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
|
||||
{ msgMeta :: MsgMeta,
|
||||
msgBody :: MsgBody,
|
||||
internalRcvId :: InternalRcvId,
|
||||
internalHash :: MsgHash,
|
||||
externalPrevSndHash :: MsgHash
|
||||
}
|
||||
|
||||
data SndMsgData = SndMsgData
|
||||
{ internalId :: InternalId,
|
||||
internalSndId :: InternalSndId,
|
||||
internalTs :: InternalTs,
|
||||
msgBody :: MsgBody,
|
||||
internalHash :: MsgHash,
|
||||
previousMsgHash :: MsgHash
|
||||
}
|
||||
|
||||
data PendingMsg = PendingMsg
|
||||
{ connId :: ConnId,
|
||||
msgId :: InternalId
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- * Broadcast types
|
||||
|
||||
type BroadcastId = ByteString
|
||||
|
||||
-- * Message types
|
||||
|
||||
-- | A message in either direction that is stored by the agent.
|
||||
@@ -149,7 +257,10 @@ data RcvMsg = RcvMsg
|
||||
-- | 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
|
||||
ackSenderTs :: AckSenderTs,
|
||||
-- | Hash of previous message as received from sender - stored for integrity forensics.
|
||||
externalPrevSndHash :: MsgHash,
|
||||
msgIntegrity :: MsgIntegrity
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -190,9 +301,9 @@ data SndMsg = SndMsg
|
||||
newtype InternalSndId = InternalSndId {unSndId :: Int64} deriving (Eq, Show)
|
||||
|
||||
data SndMsgStatus
|
||||
= Created
|
||||
| Sent
|
||||
| Delivered
|
||||
= SndMsgCreated
|
||||
| SndMsgSent
|
||||
| SndMsgDelivered
|
||||
deriving (Eq, Show)
|
||||
|
||||
type SentTs = UTCTime
|
||||
@@ -201,7 +312,7 @@ type DeliveredTs = UTCTime
|
||||
|
||||
-- | Base message data independent of direction.
|
||||
data MsgBase = MsgBase
|
||||
{ connAlias :: ConnAlias,
|
||||
{ connAlias :: ConnId,
|
||||
-- | 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)
|
||||
@@ -209,7 +320,9 @@ data MsgBase = MsgBase
|
||||
-- due to a possibility of implementation errors in different agents.
|
||||
internalId :: InternalId,
|
||||
internalTs :: InternalTs,
|
||||
msgBody :: MsgBody
|
||||
msgBody :: MsgBody,
|
||||
-- | Hash of the message as computed by agent.
|
||||
internalHash :: MsgHash
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -219,13 +332,29 @@ type InternalTs = UTCTime
|
||||
|
||||
-- * Store errors
|
||||
|
||||
-- TODO revise
|
||||
-- | Agent store error.
|
||||
data StoreError
|
||||
= SEInternal
|
||||
| SENotFound
|
||||
| SEBadConn
|
||||
| SEBadConnType ConnType
|
||||
| SEBadQueueStatus
|
||||
| SEBadQueueDirection
|
||||
| SENotImplemented -- TODO remove
|
||||
= -- | IO exceptions in store actions.
|
||||
SEInternal ByteString
|
||||
| -- | failed to generate unique random ID
|
||||
SEUniqueID
|
||||
| -- | Connection alias not found (or both queues absent).
|
||||
SEConnNotFound
|
||||
| -- | Connection alias already used.
|
||||
SEConnDuplicate
|
||||
| -- | Wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa.
|
||||
-- 'upgradeRcvConnToDuplex' and 'upgradeSndConnToDuplex' do not allow duplex connections - they would also return this error.
|
||||
SEBadConnType ConnType
|
||||
| -- | Confirmation not found.
|
||||
SEConfirmationNotFound
|
||||
| -- | Invitation not found
|
||||
SEInvitationNotFound
|
||||
| -- | Message not found
|
||||
SEMsgNotFound
|
||||
| -- | Currently not used. 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.
|
||||
SEBadQueueStatus
|
||||
| -- | Used in `getMsg` that is not implemented/used. TODO remove.
|
||||
SENotImplemented
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
( Migration (..),
|
||||
app,
|
||||
initialize,
|
||||
get,
|
||||
run,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (forM_)
|
||||
import Data.FileEmbed (embedDir, makeRelativeToProject)
|
||||
import Data.Function (on)
|
||||
import Data.List (intercalate, sortBy)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Database.SQLite.Simple (Connection, Only (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import qualified Database.SQLite3 as SQLite3
|
||||
import System.FilePath (takeBaseName, takeExtension)
|
||||
|
||||
data Migration = Migration {name :: String, up :: Text}
|
||||
deriving (Show)
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
app :: [Migration]
|
||||
app =
|
||||
sortBy (compare `on` name) . map migration . filter sqlFile $
|
||||
$(makeRelativeToProject "migrations" >>= embedDir)
|
||||
where
|
||||
sqlFile (file, _) = takeExtension file == ".sql"
|
||||
migration (file, qStr) = Migration {name = takeBaseName file, up = decodeUtf8 qStr}
|
||||
|
||||
get :: Connection -> [Migration] -> IO (Either String [Migration])
|
||||
get conn migrations =
|
||||
migrationsToRun migrations . map fromOnly
|
||||
<$> DB.query_ conn "SELECT name FROM migrations ORDER BY name ASC;"
|
||||
|
||||
run :: Connection -> [Migration] -> IO ()
|
||||
run conn ms = DB.withImmediateTransaction conn . forM_ ms $
|
||||
\Migration {name, up} -> insert name >> execSQL up
|
||||
where
|
||||
insert name = DB.execute conn "INSERT INTO migrations (name, ts) VALUES (?, ?);" . (name,) =<< getCurrentTime
|
||||
execSQL = SQLite3.exec $ DB.connectionHandle conn
|
||||
|
||||
initialize :: Connection -> IO ()
|
||||
initialize conn =
|
||||
DB.execute_
|
||||
conn
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
name TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
PRIMARY KEY (name)
|
||||
);
|
||||
|]
|
||||
|
||||
migrationsToRun :: [Migration] -> [String] -> Either String [Migration]
|
||||
migrationsToRun appMs [] = Right appMs
|
||||
migrationsToRun [] dbMs = Left $ "database version is newer than the app: " <> intercalate ", " dbMs
|
||||
migrationsToRun (a : as) (d : ds)
|
||||
| name a == d = migrationsToRun as ds
|
||||
| otherwise = Left $ "different migration in the app/database: " <> name a <> " / " <> d
|
||||
@@ -1,160 +0,0 @@
|
||||
{-# 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,
|
||||
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,
|
||||
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,
|
||||
PRIMARY KEY (conn_alias, internal_snd_id),
|
||||
FOREIGN KEY (conn_alias, internal_id)
|
||||
REFERENCES messages (conn_alias, internal_id)
|
||||
ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|]
|
||||
@@ -1,437 +0,0 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# 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 ((<|>))
|
||||
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
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.ISO8601
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable ()
|
||||
import Network.Socket
|
||||
import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol
|
||||
( CorrId (..),
|
||||
Encoded,
|
||||
ErrorType,
|
||||
MsgBody,
|
||||
MsgId,
|
||||
SenderPublicKey,
|
||||
errMessageBody,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util
|
||||
import System.IO
|
||||
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 ::
|
||||
{ m_recipient :: (AgentMsgId, UTCTime),
|
||||
m_broker :: (MsgId, UTCTime),
|
||||
m_sender :: (AgentMsgId, UTCTime),
|
||||
m_status :: MsgStatus,
|
||||
m_body :: 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) $ SYNTAX errBadMessage
|
||||
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
|
||||
<*> base64P <* 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 [B.pack $ show 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_MSG <$> (A.endOfLine *> 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 <*> port <*> msgHash
|
||||
where
|
||||
server = B.unpack <$> A.takeTill (A.inClass ":# ")
|
||||
port = A.char ':' *> (Just . show <$> (A.decimal :: Parser Int)) <|> pure Nothing
|
||||
msgHash = A.char '#' *> (Just <$> base64P) <|> pure Nothing
|
||||
|
||||
parseAgentMessage :: ByteString -> Either AgentErrorType AMessage
|
||||
parseAgentMessage = parse agentMessageP $ SYNTAX errBadMessage
|
||||
|
||||
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) keyHash
|
||||
|
||||
data SMPServer = SMPServer
|
||||
{ host :: HostName,
|
||||
port :: Maybe ServiceName,
|
||||
keyHash :: Maybe KeyHash
|
||||
}
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
type KeyHash = Encoded
|
||||
|
||||
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.PrivateKey
|
||||
|
||||
type SignatureKey = C.PrivateKey
|
||||
|
||||
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 MsgStatus = MsgOk | MsgError MsgErrorType
|
||||
deriving (Eq, Show)
|
||||
|
||||
data MsgErrorType = MsgSkipped AgentMsgId AgentMsgId | MsgBadId AgentMsgId | MsgBadHash
|
||||
deriving (Eq, Show)
|
||||
|
||||
data AgentErrorType
|
||||
= UNKNOWN
|
||||
| PROHIBITED
|
||||
| SYNTAX Int
|
||||
| BROKER Natural
|
||||
| SMP ErrorType
|
||||
| CRYPTO C.CryptoError
|
||||
| SIZE
|
||||
| STORE
|
||||
| INTERNAL -- etc. TODO SYNTAX Natural
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
data AckStatus = AckOk | AckError AckErrorType
|
||||
deriving (Show)
|
||||
|
||||
data AckErrorType = AckUnknown | AckProhibited | AckSyntax Int -- etc.
|
||||
deriving (Show)
|
||||
|
||||
errBadEncoding :: Int
|
||||
errBadEncoding = 10
|
||||
|
||||
errBadCommand :: Int
|
||||
errBadCommand = 11
|
||||
|
||||
errBadInvitation :: Int
|
||||
errBadInvitation = 12
|
||||
|
||||
errNoConnAlias :: Int
|
||||
errNoConnAlias = 13
|
||||
|
||||
errBadMessage :: Int
|
||||
errBadMessage = 14
|
||||
|
||||
errBadServer :: Int
|
||||
errBadServer = 15
|
||||
|
||||
smpErrTCPConnection :: Natural
|
||||
smpErrTCPConnection = 1
|
||||
|
||||
smpErrCorrelationId :: Natural
|
||||
smpErrCorrelationId = 2
|
||||
|
||||
smpUnexpectedResponse :: Natural
|
||||
smpUnexpectedResponse = 3
|
||||
|
||||
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
|
||||
m_status <- status <* A.space
|
||||
m_recipient <- "R=" *> partyMeta A.decimal
|
||||
m_broker <- "B=" *> partyMeta base64P
|
||||
m_sender <- "S=" *> partyMeta A.decimal
|
||||
m_body <- A.takeByteString
|
||||
return $ ACmd SAgent MSG {m_recipient, m_broker, m_sender, m_status, m_body}
|
||||
-- TODO other error types
|
||||
agentError = ACmd SAgent . ERR <$> ("SMP " *> smpErrorType)
|
||||
smpErrorType = "AUTH" $> SMP SMP.AUTH
|
||||
replyMode =
|
||||
" NO_REPLY" $> ReplyOff
|
||||
<|> A.space *> (ReplyVia <$> smpServerP)
|
||||
<|> pure ReplyOn
|
||||
partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space
|
||||
status = "OK" $> MsgOk <|> "ERR " *> (MsgError <$> msgErrorType)
|
||||
msgErrorType =
|
||||
"ID " *> (MsgBadId <$> A.decimal)
|
||||
<|> "NO_ID " *> (MsgSkipped <$> A.decimal <* A.space <*> A.decimal)
|
||||
<|> "HASH" $> MsgBadHash
|
||||
|
||||
parseCommand :: ByteString -> Either AgentErrorType ACmd
|
||||
parseCommand = parse commandP $ SYNTAX errBadCommand
|
||||
|
||||
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 {m_recipient = (rmId, rTs), m_broker = (bmId, bTs), m_sender = (smId, sTs), m_status, m_body} ->
|
||||
B.unwords
|
||||
[ "MSG",
|
||||
msgStatus m_status,
|
||||
"R=" <> bshow rmId <> "," <> showTs rTs,
|
||||
"B=" <> encode bmId <> "," <> showTs bTs,
|
||||
"S=" <> bshow smId <> "," <> showTs sTs,
|
||||
serializeMsg m_body
|
||||
]
|
||||
OFF -> "OFF"
|
||||
DEL -> "DEL"
|
||||
CON -> "CON"
|
||||
ERR e -> "ERR " <> B.pack (show e)
|
||||
OK -> "OK"
|
||||
where
|
||||
replyMode :: ReplyMode -> ByteString
|
||||
replyMode = \case
|
||||
ReplyOff -> " NO_REPLY"
|
||||
ReplyVia srv -> " " <> serializeServer srv
|
||||
ReplyOn -> ""
|
||||
showTs :: UTCTime -> ByteString
|
||||
showTs = B.pack . formatISO8601Millis
|
||||
msgStatus :: MsgStatus -> ByteString
|
||||
msgStatus = \case
|
||||
MsgOk -> "OK"
|
||||
MsgError e ->
|
||||
"ERR" <> case e of
|
||||
MsgSkipped fromMsgId toMsgId ->
|
||||
B.unwords ["NO_ID", B.pack $ show fromMsgId, B.pack $ show toMsgId]
|
||||
MsgBadId aMsgId -> "ID " <> B.pack (show aMsgId)
|
||||
MsgBadHash -> "HASH"
|
||||
|
||||
-- TODO - save function as in the server Transmission - re-use?
|
||||
serializeMsg :: ByteString -> ByteString
|
||||
serializeMsg body = B.pack (show $ 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 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 $ SYNTAX errNoConnAlias
|
||||
| otherwise -> Right cmd
|
||||
|
||||
cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p))
|
||||
cmdWithMsgBody = \case
|
||||
SEND body -> SEND <$$> getMsgBody body
|
||||
MSG agentMsgId srvTS agentTS status body -> MSG agentMsgId srvTS agentTS status <$$> 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 SIZE
|
||||
Nothing -> return . Left $ SYNTAX errMessageBody
|
||||
@@ -1,24 +1,43 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Client
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- This module provides a functional client API for SMP protocol.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
|
||||
module Simplex.Messaging.Client
|
||||
( SMPClient,
|
||||
( -- * Connect (disconnect) client to (from) SMP server
|
||||
SMPClient (blockSize),
|
||||
getSMPClient,
|
||||
closeSMPClient,
|
||||
|
||||
-- * SMP protocol command functions
|
||||
createSMPQueue,
|
||||
subscribeSMPQueue,
|
||||
secureSMPQueue,
|
||||
sendSMPMessage,
|
||||
ackSMPMessage,
|
||||
sendSMPCommand,
|
||||
suspendSMPQueue,
|
||||
deleteSMPQueue,
|
||||
sendSMPCommand,
|
||||
|
||||
-- * Supporting types and client configuration
|
||||
SMPClientError (..),
|
||||
SMPClientConfig (..),
|
||||
smpDefaultConfig,
|
||||
@@ -33,153 +52,209 @@ import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Trans.Class
|
||||
import Control.Monad.Trans.Except
|
||||
import qualified Crypto.PubKey.RSA.Types as RSA
|
||||
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.Maybe
|
||||
import GHC.IO.Exception (IOErrorType (..))
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Network.Socket (ServiceName)
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.Transmission (SMPServer (..))
|
||||
import Simplex.Messaging.Agent.Protocol (SMPServer (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (liftEitherError, raceAny_)
|
||||
import System.IO
|
||||
import System.IO.Error
|
||||
import System.Timeout
|
||||
import Simplex.Messaging.Transport (ATransport (..), TCP, THandle (..), TProxy, Transport (..), TransportError, clientHandshake, runTransportClient)
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (bshow, liftError, raceAny_)
|
||||
import System.Timeout (timeout)
|
||||
|
||||
-- | 'SMPClient' is a handle used to send commands to a specific SMP server.
|
||||
--
|
||||
-- The only exported selector is blockSize that is negotiated
|
||||
-- with the server during the TCP transport handshake.
|
||||
--
|
||||
-- Use 'getSMPClient' to connect to an SMP server and create a client handle.
|
||||
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
|
||||
msgQ :: TBQueue SMPServerTransmission,
|
||||
blockSize :: Int
|
||||
}
|
||||
|
||||
-- | Type synonym for transmission from some SPM server queue.
|
||||
type SMPServerTransmission = (SMPServer, RecipientId, Command 'Broker)
|
||||
|
||||
-- | SMP client configuration.
|
||||
data SMPClientConfig = SMPClientConfig
|
||||
{ qSize :: Natural,
|
||||
defaultPort :: ServiceName,
|
||||
{ -- | size of TBQueue to use for server commands and responses
|
||||
qSize :: Natural,
|
||||
-- | default SMP server port if port is not specified in SMPServer
|
||||
defaultTransport :: (ServiceName, ATransport),
|
||||
-- | timeout of TCP commands (microseconds)
|
||||
tcpTimeout :: Int,
|
||||
-- | period for SMP ping commands (microseconds)
|
||||
smpPing :: Int,
|
||||
blockSize :: Int,
|
||||
-- | SMP transport block size, Nothing - the block size will be set by the server.
|
||||
-- Allowed sizes are 4, 8, 16, 32, 64 KiB (* 1024 bytes).
|
||||
smpBlockSize :: Maybe Int,
|
||||
-- | estimated maximum size of SMP command excluding message body,
|
||||
-- determines the maximum allowed message size
|
||||
smpCommandSize :: Int
|
||||
}
|
||||
|
||||
-- | Default SMP client configuration.
|
||||
smpDefaultConfig :: SMPClientConfig
|
||||
smpDefaultConfig =
|
||||
SMPClientConfig
|
||||
{ qSize = 16,
|
||||
defaultPort = "5223",
|
||||
tcpTimeout = 2_000_000,
|
||||
defaultTransport = ("5223", transport @TCP),
|
||||
tcpTimeout = 4_000_000,
|
||||
smpPing = 30_000_000,
|
||||
blockSize = 8_192, -- 16_384,
|
||||
smpBlockSize = Just 8192,
|
||||
smpCommandSize = 256
|
||||
}
|
||||
|
||||
data Request = Request
|
||||
{ queueId :: QueueId,
|
||||
responseVar :: TMVar (Either SMPClientError Cmd)
|
||||
responseVar :: TMVar Response
|
||||
}
|
||||
|
||||
getSMPClient :: SMPServer -> SMPClientConfig -> TBQueue SMPServerTransmission -> IO () -> IO SMPClient
|
||||
getSMPClient
|
||||
smpServer@SMPServer {host, port}
|
||||
SMPClientConfig {qSize, defaultPort, tcpTimeout, smpPing}
|
||||
msgQ
|
||||
disconnected = do
|
||||
c <- atomically mkSMPClient
|
||||
started <- newEmptyTMVarIO
|
||||
action <-
|
||||
async $
|
||||
runTCPClient host (fromMaybe defaultPort port) (client c started)
|
||||
`finally` atomically (putTMVar started False)
|
||||
tcpTimeout `timeout` atomically (takeTMVar started) >>= \case
|
||||
Just True -> return c {action}
|
||||
_ -> throwIO err
|
||||
where
|
||||
err :: IOException
|
||||
err = mkIOError TimeExpired "connection timeout" Nothing Nothing
|
||||
type Response = Either SMPClientError Cmd
|
||||
|
||||
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,
|
||||
connected,
|
||||
smpServer,
|
||||
clientCorrId,
|
||||
sentCommands,
|
||||
sndQ,
|
||||
rcvQ,
|
||||
msgQ
|
||||
}
|
||||
-- | Connects to 'SMPServer' using passed client configuration
|
||||
-- and queue for messages and notifications.
|
||||
--
|
||||
-- A single queue can be used for multiple 'SMPClient' instances,
|
||||
-- as 'SMPServerTransmission' includes server information.
|
||||
getSMPClient :: SMPServer -> SMPClientConfig -> TBQueue SMPServerTransmission -> IO () -> IO (Either SMPClientError SMPClient)
|
||||
getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing, smpBlockSize} msgQ disconnected =
|
||||
atomically mkSMPClient >>= runClient useTransport
|
||||
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 Bool -> Handle -> IO ()
|
||||
client c started h = do
|
||||
_ <- getLn h -- "Welcome to SMP"
|
||||
atomically $ do
|
||||
modifyTVar (connected c) (const True)
|
||||
putTMVar started True
|
||||
raceAny_ [send c h, process c, receive c h, ping c]
|
||||
`finally` disconnected
|
||||
runClient :: (ServiceName, ATransport) -> SMPClient -> IO (Either SMPClientError SMPClient)
|
||||
runClient (port', ATransport t) c = do
|
||||
thVar <- newEmptyTMVarIO
|
||||
action <-
|
||||
async $
|
||||
runTransportClient (host smpServer) port' (client t c thVar)
|
||||
`finally` atomically (putTMVar thVar $ Left SMPNetworkError)
|
||||
bSize <- tcpTimeout `timeout` atomically (takeTMVar thVar)
|
||||
pure $ case bSize of
|
||||
Just (Right blockSize) -> Right c {action, blockSize}
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left SMPNetworkError
|
||||
|
||||
send :: SMPClient -> Handle -> IO ()
|
||||
send SMPClient {sndQ} h = forever $ atomically (readTBQueue sndQ) >>= tPut h
|
||||
useTransport :: (ServiceName, ATransport)
|
||||
useTransport = case port smpServer of
|
||||
Nothing -> defaultTransport cfg
|
||||
Just "80" -> ("80", transport @WS)
|
||||
Just p -> (p, transport @TCP)
|
||||
|
||||
receive :: SMPClient -> Handle -> IO ()
|
||||
receive SMPClient {rcvQ} h = forever $ tGet fromServer h >>= atomically . writeTBQueue rcvQ
|
||||
client :: forall c. Transport c => TProxy c -> SMPClient -> TMVar (Either SMPClientError Int) -> c -> IO ()
|
||||
client _ c thVar h =
|
||||
runExceptT (clientHandshake h smpBlockSize $ keyHash smpServer) >>= \case
|
||||
Left e -> atomically . putTMVar thVar . Left $ SMPTransportError e
|
||||
Right th -> do
|
||||
atomically $ do
|
||||
writeTVar (connected c) True
|
||||
putTMVar thVar . Right $ blockSize (th :: THandle c)
|
||||
raceAny_ [send c th, process c, receive c th, ping c]
|
||||
`finally` disconnected
|
||||
|
||||
ping :: SMPClient -> IO ()
|
||||
ping c = forever $ do
|
||||
threadDelay smpPing
|
||||
runExceptT $ sendSMPCommand c Nothing "" (Cmd SSender PING)
|
||||
send :: Transport c => SMPClient -> THandle c -> IO ()
|
||||
send SMPClient {sndQ} h = forever $ atomically (readTBQueue sndQ) >>= tPut h
|
||||
|
||||
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 SMPQueueIdError
|
||||
receive :: Transport c => SMPClient -> THandle c -> 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
|
||||
if B.null $ bs corrId
|
||||
then sendMsg qId respOrErr
|
||||
else do
|
||||
cs <- readTVarIO sentCommands
|
||||
case M.lookup corrId cs of
|
||||
Nothing -> sendMsg qId respOrErr
|
||||
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
|
||||
|
||||
sendMsg :: QueueId -> Either ErrorType Cmd -> IO ()
|
||||
sendMsg qId = \case
|
||||
Right (Cmd SBroker cmd) -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd)
|
||||
-- TODO send everything else to errQ and log in agent
|
||||
_ -> return ()
|
||||
|
||||
-- | Disconnects SMP client from the server and terminates client threads.
|
||||
closeSMPClient :: SMPClient -> IO ()
|
||||
closeSMPClient = uninterruptibleCancel . action
|
||||
|
||||
-- | SMP client error type.
|
||||
data SMPClientError
|
||||
= SMPServerError ErrorType
|
||||
| SMPResponseError ErrorType
|
||||
| SMPQueueIdError
|
||||
| SMPUnexpectedResponse
|
||||
| SMPResponseTimeout
|
||||
| SMPCryptoError RSA.Error
|
||||
| SMPClientError
|
||||
= -- | Correctly parsed SMP server ERR response.
|
||||
-- This error is forwarded to the agent client as `ERR SMP err`.
|
||||
SMPServerError ErrorType
|
||||
| -- | Invalid server response that failed to parse.
|
||||
-- Forwarded to the agent client as `ERR BROKER RESPONSE`.
|
||||
SMPResponseError ErrorType
|
||||
| -- | Different response from what is expected to a certain SMP command,
|
||||
-- e.g. server should respond `IDS` or `ERR` to `NEW` command,
|
||||
-- other responses would result in this error.
|
||||
-- Forwarded to the agent client as `ERR BROKER UNEXPECTED`.
|
||||
SMPUnexpectedResponse
|
||||
| -- | Used for TCP connection and command response timeouts.
|
||||
-- Forwarded to the agent client as `ERR BROKER TIMEOUT`.
|
||||
SMPResponseTimeout
|
||||
| -- | Failure to establish TCP connection.
|
||||
-- Forwarded to the agent client as `ERR BROKER NETWORK`.
|
||||
SMPNetworkError
|
||||
| -- | TCP transport handshake or some other transport error.
|
||||
-- Forwarded to the agent client as `ERR BROKER TRANSPORT e`.
|
||||
SMPTransportError TransportError
|
||||
| -- | Error when cryptographically "signing" the command.
|
||||
SMPSignatureError C.CryptoError
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
-- | Create a new SMP queue.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command
|
||||
createSMPQueue ::
|
||||
SMPClient ->
|
||||
RecipientPrivateKey ->
|
||||
@@ -191,6 +266,9 @@ createSMPQueue c rpKey rKey =
|
||||
Cmd _ (IDS rId sId) -> return (rId, sId)
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
-- | Subscribe to the SMP queue.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue
|
||||
subscribeSMPQueue :: SMPClient -> RecipientPrivateKey -> RecipientId -> ExceptT SMPClientError IO ()
|
||||
subscribeSMPQueue c@SMPClient {smpServer, msgQ} rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId (Cmd SRecipient SUB) >>= \case
|
||||
@@ -199,15 +277,24 @@ subscribeSMPQueue c@SMPClient {smpServer, msgQ} rpKey rId =
|
||||
lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
-- | Secure the SMP queue by adding a sender public key.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#secure-queue-command
|
||||
secureSMPQueue :: SMPClient -> RecipientPrivateKey -> RecipientId -> SenderPublicKey -> ExceptT SMPClientError IO ()
|
||||
secureSMPQueue c rpKey rId senderKey = okSMPCommand (Cmd SRecipient $ KEY senderKey) c rpKey rId
|
||||
|
||||
-- | Send SMP message.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#send-message
|
||||
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
|
||||
|
||||
-- | Acknowledge message delivery (server deletes the message).
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
|
||||
ackSMPMessage :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
ackSMPMessage c@SMPClient {smpServer, msgQ} rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId (Cmd SRecipient ACK) >>= \case
|
||||
@@ -216,20 +303,28 @@ ackSMPMessage c@SMPClient {smpServer, msgQ} rpKey rId =
|
||||
lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
|
||||
_ -> throwE SMPUnexpectedResponse
|
||||
|
||||
-- | Irreversibly suspend SMP queue.
|
||||
-- The existing messages from the queue will still be delivered.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#suspend-queue
|
||||
suspendSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
suspendSMPQueue = okSMPCommand $ Cmd SRecipient OFF
|
||||
|
||||
-- | Irreversibly delete SMP queue and all messages in it.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#delete-queue
|
||||
deleteSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
deleteSMPQueue = okSMPCommand $ Cmd SRecipient DEL
|
||||
|
||||
okSMPCommand :: Cmd -> SMPClient -> C.PrivateKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
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.PrivateKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd
|
||||
sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId} pKey qId cmd = do
|
||||
-- | Send any SMP command ('Cmd' type).
|
||||
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
|
||||
@@ -239,22 +334,23 @@ sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId} pKey qId cmd = do
|
||||
|
||||
getNextCorrId :: STM CorrId
|
||||
getNextCorrId = do
|
||||
i <- (+ 1) <$> readTVar clientCorrId
|
||||
writeTVar clientCorrId i
|
||||
return . CorrId . B.pack $ show i
|
||||
i <- stateTVar clientCorrId $ \i -> (i, i + 1)
|
||||
pure . CorrId $ bshow i
|
||||
|
||||
signTransmission :: ByteString -> ExceptT SMPClientError IO SignedRawTransmission
|
||||
signTransmission t = case pKey of
|
||||
Nothing -> return ("", t)
|
||||
Just pk -> do
|
||||
sig <- liftEitherError SMPCryptoError $ C.sign pk t
|
||||
sig <- liftError SMPSignatureError $ C.sign pk t
|
||||
return (sig, t)
|
||||
|
||||
-- two separate "atomically" needed to avoid blocking
|
||||
sendRecv :: CorrId -> SignedRawTransmission -> IO (Either SMPClientError Cmd)
|
||||
sendRecv corrId t = atomically (send corrId t) >>= atomically . takeTMVar
|
||||
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 (Either SMPClientError Cmd))
|
||||
send :: CorrId -> SignedRawTransmission -> STM (TMVar Response)
|
||||
send corrId t = do
|
||||
r <- newEmptyTMVar
|
||||
modifyTVar sentCommands . M.insert corrId $ Request qId r
|
||||
|
||||
@@ -1,26 +1,82 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Crypto
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- This module provides cryptography implementation for SMP protocols based on
|
||||
-- <https://hackage.haskell.org/package/cryptonite cryptonite package>.
|
||||
module Simplex.Messaging.Crypto
|
||||
( PrivateKey (..),
|
||||
( -- * RSA keys
|
||||
PrivateKey (rsaPrivateKey, publicKey),
|
||||
SafePrivateKey (..), -- constructor is not exported
|
||||
FullPrivateKey (..),
|
||||
APrivateKey (..),
|
||||
PublicKey (..),
|
||||
Signature (..),
|
||||
CryptoError (..),
|
||||
SafeKeyPair,
|
||||
FullKeyPair,
|
||||
KeyHash (..),
|
||||
generateKeyPair,
|
||||
sign,
|
||||
verify,
|
||||
publicKey',
|
||||
publicKeySize,
|
||||
validKeySize,
|
||||
safePrivateKey,
|
||||
removePublicKey,
|
||||
|
||||
-- * E2E hybrid encryption scheme
|
||||
encrypt,
|
||||
decrypt,
|
||||
|
||||
-- * RSA OAEP encryption
|
||||
encryptOAEP,
|
||||
decryptOAEP,
|
||||
|
||||
-- * RSA PSS signing
|
||||
Signature (..),
|
||||
sign,
|
||||
verify,
|
||||
|
||||
-- * AES256 AEAD-GCM scheme
|
||||
Key (..),
|
||||
IV (..),
|
||||
encryptAES,
|
||||
decryptAES,
|
||||
authTagSize,
|
||||
authTagToBS,
|
||||
bsToAuthTag,
|
||||
randomAesKey,
|
||||
randomIV,
|
||||
aesKeyP,
|
||||
ivP,
|
||||
|
||||
-- * Encoding of RSA keys
|
||||
serializePrivKey,
|
||||
serializePubKey,
|
||||
parsePrivKey,
|
||||
parsePubKey,
|
||||
serializePubKeyUri,
|
||||
encodePubKey,
|
||||
publicKeyHash,
|
||||
privKeyP,
|
||||
pubKeyP,
|
||||
pubKeyUriP,
|
||||
binaryPubKeyP,
|
||||
|
||||
-- * SHA256 hash
|
||||
sha256Hash,
|
||||
|
||||
-- * Cryptography error type
|
||||
CryptoError (..),
|
||||
)
|
||||
where
|
||||
|
||||
@@ -30,75 +86,142 @@ 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.Algorithms (SHA256 (..))
|
||||
import Crypto.Hash (Digest, SHA256 (..), hash)
|
||||
import Crypto.Number.Generate (generateMax)
|
||||
import Crypto.Number.Prime (findPrimeFrom)
|
||||
import Crypto.Number.Serialize (i2osp, os2ip)
|
||||
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 (first)
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Base64 (decode, encode)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
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 Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.Internal (Field (..))
|
||||
import Database.SQLite.Simple.Ok (Ok (Ok))
|
||||
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)
|
||||
import Simplex.Messaging.Util (bshow, liftEitherError, (<$$>))
|
||||
import Simplex.Messaging.Parsers (base64P, base64UriP, blobFieldParser, parseAll, parseString)
|
||||
import Simplex.Messaging.Util (liftEitherError, (<$?>))
|
||||
|
||||
-- | A newtype of 'Crypto.PubKey.RSA.PublicKey'.
|
||||
newtype PublicKey = PublicKey {rsaPublicKey :: R.PublicKey} deriving (Eq, Show)
|
||||
|
||||
data PrivateKey = PrivateKey
|
||||
{ private_size :: Int,
|
||||
private_n :: Integer,
|
||||
private_d :: Integer
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
-- | A newtype of 'Crypto.PubKey.RSA.PrivateKey', with PublicKey removed.
|
||||
--
|
||||
-- It is not possible to recover PublicKey from SafePrivateKey.
|
||||
-- The constructor of this type is not exported.
|
||||
newtype SafePrivateKey = SafePrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show)
|
||||
|
||||
instance ToField PrivateKey where toField = toField . serializePrivKey
|
||||
-- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (with PublicKey inside).
|
||||
newtype FullPrivateKey = FullPrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show)
|
||||
|
||||
instance ToField PublicKey where toField = toField . serializePubKey
|
||||
-- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (PublicKey may be inside).
|
||||
newtype APrivateKey = APrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show)
|
||||
|
||||
instance FromField PrivateKey where
|
||||
fromField f@(Field (SQLBlob b) _) =
|
||||
case parsePrivKey b of
|
||||
Right k -> Ok k
|
||||
Left e -> returnError ConversionFailed f ("couldn't parse PrivateKey field: " ++ e)
|
||||
fromField f = returnError ConversionFailed f "expecting SQLBlob column type"
|
||||
-- | Type-class used for both private key types: SafePrivateKey and FullPrivateKey.
|
||||
class PrivateKey k where
|
||||
-- unwraps 'Crypto.PubKey.RSA.PrivateKey'
|
||||
rsaPrivateKey :: k -> R.PrivateKey
|
||||
|
||||
instance FromField PublicKey where
|
||||
fromField f@(Field (SQLBlob b) _) =
|
||||
case parsePubKey b of
|
||||
Right k -> Ok k
|
||||
Left e -> returnError ConversionFailed f ("couldn't parse PublicKey field: " ++ e)
|
||||
fromField f = returnError ConversionFailed f "expecting SQLBlob column type"
|
||||
-- equivalent to data type constructor, not exported
|
||||
_privateKey :: R.PrivateKey -> k
|
||||
|
||||
type KeyPair = (PublicKey, PrivateKey)
|
||||
-- smart constructor removing public key from SafePrivateKey but keeping it in FullPrivateKey
|
||||
mkPrivateKey :: R.PrivateKey -> k
|
||||
|
||||
-- extracts public key from private key
|
||||
publicKey :: k -> Maybe PublicKey
|
||||
|
||||
-- | Remove public key exponent from APrivateKey.
|
||||
removePublicKey :: APrivateKey -> APrivateKey
|
||||
removePublicKey (APrivateKey R.PrivateKey {private_pub = k, private_d}) =
|
||||
APrivateKey $ unPrivateKey (safePrivateKey (R.public_size k, R.public_n k, private_d) :: SafePrivateKey)
|
||||
|
||||
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)
|
||||
publicKey _ = Nothing
|
||||
|
||||
instance PrivateKey FullPrivateKey where
|
||||
rsaPrivateKey = unPrivateKey
|
||||
_privateKey = FullPrivateKey
|
||||
mkPrivateKey = FullPrivateKey
|
||||
publicKey = Just . PublicKey . R.private_pub . rsaPrivateKey
|
||||
|
||||
instance PrivateKey APrivateKey where
|
||||
rsaPrivateKey = unPrivateKey
|
||||
_privateKey = APrivateKey
|
||||
mkPrivateKey = APrivateKey
|
||||
publicKey pk =
|
||||
let k = R.private_pub $ rsaPrivateKey pk
|
||||
in if R.public_e k == 0
|
||||
then Nothing
|
||||
else Just $ PublicKey k
|
||||
|
||||
instance IsString FullPrivateKey where
|
||||
fromString = parseString $ decode >=> decodePrivKey
|
||||
|
||||
instance IsString PublicKey where
|
||||
fromString = parseString $ decode >=> decodePubKey
|
||||
|
||||
instance ToField SafePrivateKey where toField = toField . encodePrivKey
|
||||
|
||||
instance ToField APrivateKey where toField = toField . encodePrivKey
|
||||
|
||||
instance ToField PublicKey where toField = toField . encodePubKey
|
||||
|
||||
instance FromField SafePrivateKey where fromField = blobFieldParser binaryPrivKeyP
|
||||
|
||||
instance FromField APrivateKey where fromField = blobFieldParser binaryPrivKeyP
|
||||
|
||||
instance FromField PublicKey where fromField = blobFieldParser binaryPubKeyP
|
||||
|
||||
-- | Tuple of RSA 'PublicKey' and 'PrivateKey'.
|
||||
type KeyPair k = (PublicKey, k)
|
||||
|
||||
-- | Tuple of RSA 'PublicKey' and 'SafePrivateKey'.
|
||||
type SafeKeyPair = (PublicKey, SafePrivateKey)
|
||||
|
||||
-- | Tuple of RSA 'PublicKey' and 'FullPrivateKey'.
|
||||
type FullKeyPair = (PublicKey, FullPrivateKey)
|
||||
|
||||
-- | RSA signature newtype.
|
||||
newtype Signature = Signature {unSignature :: ByteString} deriving (Eq, Show)
|
||||
|
||||
instance IsString Signature where
|
||||
fromString = Signature . fromString
|
||||
|
||||
newtype Verified = Verified ByteString deriving (Show)
|
||||
|
||||
-- | Various cryptographic or related errors.
|
||||
data CryptoError
|
||||
= CryptoRSAError R.Error
|
||||
| CryptoCipherError CE.CryptoError
|
||||
| CryptoIVError
|
||||
| CryptoDecryptError
|
||||
| CryptoLargeMsgError
|
||||
| CryptoHeaderError String
|
||||
= -- | RSA OAEP encryption error
|
||||
RSAEncryptError R.Error
|
||||
| -- | RSA OAEP decryption error
|
||||
RSADecryptError R.Error
|
||||
| -- | RSA PSS signature error
|
||||
RSASignError R.Error
|
||||
| -- | AES initialization error
|
||||
AESCipherError CE.CryptoError
|
||||
| -- | IV generation error
|
||||
CryptoIVError
|
||||
| -- | AES decryption error
|
||||
AESDecryptError
|
||||
| -- | message does not fit in SMP block
|
||||
CryptoLargeMsgError
|
||||
| -- | failure parsing RSA-encrypted message header
|
||||
CryptoHeaderError String
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
pubExpRange :: Integer
|
||||
@@ -110,19 +233,35 @@ aesKeySize = 256 `div` 8
|
||||
authTagSize :: Int
|
||||
authTagSize = 128 `div` 8
|
||||
|
||||
generateKeyPair :: Int -> IO KeyPair
|
||||
-- | Generate RSA key pair with either SafePrivateKey or FullPrivateKey.
|
||||
generateKeyPair :: PrivateKey k => Int -> IO (KeyPair k)
|
||||
generateKeyPair size = loop
|
||||
where
|
||||
publicExponent = findPrimeFrom . (+ 3) <$> generateMax pubExpRange
|
||||
privateKey s n d = PrivateKey {private_size = s, private_n = n, private_d = d}
|
||||
loop = do
|
||||
(pub, priv) <- R.generate size =<< publicExponent
|
||||
let s = R.public_size pub
|
||||
n = R.public_n pub
|
||||
d = R.private_d priv
|
||||
in if d * d < n
|
||||
then loop
|
||||
else return (PublicKey pub, privateKey s n d)
|
||||
(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
|
||||
384 -> True
|
||||
512 -> True
|
||||
_ -> False
|
||||
|
||||
data Header = Header
|
||||
{ aesKey :: Key,
|
||||
@@ -131,56 +270,99 @@ data Header = Header
|
||||
msgSize :: Int
|
||||
}
|
||||
|
||||
-- | AES key newtype.
|
||||
newtype Key = Key {unKey :: ByteString}
|
||||
|
||||
-- | IV bytes newtype.
|
||||
newtype IV = IV {unIV :: ByteString}
|
||||
|
||||
-- | Key hash newtype.
|
||||
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
|
||||
|
||||
-- | Digest (hash) of binary X509 encoding of RSA public key.
|
||||
publicKeyHash :: PublicKey -> KeyHash
|
||||
publicKeyHash = KeyHash . sha256Hash . encodePubKey
|
||||
|
||||
-- | SHA256 digest.
|
||||
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 <- Key <$> A.take aesKeySize
|
||||
ivBytes <- IV <$> A.take (ivSize @AES256)
|
||||
aesKey <- aesKeyP
|
||||
ivBytes <- ivP
|
||||
authTag <- bsToAuthTag <$> A.take authTagSize
|
||||
msgSize <- fromIntegral . decodeWord32 <$> A.take 4
|
||||
return Header {aesKey, ivBytes, authTag, msgSize}
|
||||
|
||||
parseHeader :: ByteString -> Either CryptoError Header
|
||||
parseHeader = first CryptoHeaderError . A.parseOnly (headerP <* A.endOfInput)
|
||||
-- | AES256 key parser.
|
||||
aesKeyP :: Parser Key
|
||||
aesKeyP = Key <$> A.take aesKeySize
|
||||
|
||||
-- | IV bytes parser.
|
||||
ivP :: Parser IV
|
||||
ivP = IV <$> A.take (ivSize @AES256)
|
||||
|
||||
parseHeader :: ByteString -> Either CryptoError Header
|
||||
parseHeader = first CryptoHeaderError . parseAll headerP
|
||||
|
||||
-- * E2E hybrid encryption scheme
|
||||
|
||||
-- | E2E encrypt SMP agent messages.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption
|
||||
encrypt :: PublicKey -> Int -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
encrypt k paddedSize msg = do
|
||||
aesKey <- Key <$> randomBytes aesKeySize
|
||||
ivBytes <- IV <$> randomBytes (ivSize @AES256)
|
||||
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'
|
||||
|
||||
-- | E2E decrypt SMP agent messages.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption
|
||||
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
|
||||
|
||||
-- | AEAD-GCM encryption.
|
||||
--
|
||||
-- Used as part of hybrid E2E encryption scheme and for SMP transport blocks encryption.
|
||||
encryptAES :: Key -> IV -> Int -> ByteString -> ExceptT CryptoError IO (AES.AuthTag, ByteString)
|
||||
encryptAES aesKey ivBytes paddedSize msg = do
|
||||
aead <- initAEAD @AES256 aesKey ivBytes
|
||||
msg' <- paddedMsg
|
||||
let (authTag, msg'') = encryptAES aead msg'
|
||||
header = Header {aesKey, ivBytes, authTag, msgSize = B.length msg}
|
||||
encHeader <- encryptOAEP k $ serializeHeader header
|
||||
return $ encHeader <> msg''
|
||||
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) '#')
|
||||
|
||||
decrypt :: PrivateKey -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
decrypt pk msg'' = do
|
||||
let (encHeader, msg') = B.splitAt (private_size pk) msg''
|
||||
header <- decryptOAEP pk encHeader
|
||||
Header {aesKey, ivBytes, authTag, msgSize} <- ExceptT . return $ parseHeader header
|
||||
-- | AEAD-GCM decryption.
|
||||
--
|
||||
-- Used as part of hybrid E2E encryption scheme and for SMP transport blocks decryption.
|
||||
decryptAES :: Key -> IV -> ByteString -> AES.AuthTag -> ExceptT CryptoError IO ByteString
|
||||
decryptAES aesKey ivBytes msg authTag = do
|
||||
aead <- initAEAD @AES256 aesKey ivBytes
|
||||
msg <- decryptAES aead msg' authTag
|
||||
return $ B.take msgSize msg
|
||||
|
||||
encryptAES :: AES.AEAD AES256 -> ByteString -> (AES.AuthTag, ByteString)
|
||||
encryptAES aead plaintext = AES.aeadSimpleEncrypt aead B.empty plaintext authTagSize
|
||||
|
||||
decryptAES :: AES.AEAD AES256 -> ByteString -> AES.AuthTag -> ExceptT CryptoError IO ByteString
|
||||
decryptAES aead ciphertext authTag =
|
||||
maybeError CryptoDecryptError $ AES.aeadSimpleDecrypt aead B.empty ciphertext authTag
|
||||
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
|
||||
@@ -189,94 +371,152 @@ initAEAD (Key aesKey) (IV ivBytes) = do
|
||||
cipher <- AES.cipherInit aesKey
|
||||
AES.aeadInit AES.AEAD_GCM cipher iv
|
||||
|
||||
-- | Random AES256 key.
|
||||
randomAesKey :: IO Key
|
||||
randomAesKey = Key <$> getRandomBytes aesKeySize
|
||||
|
||||
-- | Random IV bytes for AES256 encryption.
|
||||
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
|
||||
|
||||
randomBytes :: Int -> ExceptT CryptoError IO ByteString
|
||||
randomBytes n = ExceptT $ Right <$> getRandomBytes n
|
||||
|
||||
maybeError :: CryptoError -> Maybe a -> ExceptT CryptoError IO a
|
||||
maybeError e = maybe (throwE e) return
|
||||
|
||||
-- | Convert AEAD 'AuthTag' to ByteString.
|
||||
authTagToBS :: AES.AuthTag -> ByteString
|
||||
authTagToBS = B.pack . map w2c . BA.unpack . AES.unAuthTag
|
||||
|
||||
-- | Convert ByteString to AEAD 'AuthTag'.
|
||||
bsToAuthTag :: ByteString -> AES.AuthTag
|
||||
bsToAuthTag = AES.AuthTag . BA.pack . map c2w . B.unpack
|
||||
|
||||
cryptoFailable :: CE.CryptoFailable a -> ExceptT CryptoError IO a
|
||||
cryptoFailable = liftEither . first CryptoCipherError . CE.eitherCryptoError
|
||||
cryptoFailable = liftEither . first AESCipherError . CE.eitherCryptoError
|
||||
|
||||
oaepParams :: OAEP.OAEPParams SHA256 ByteString ByteString
|
||||
oaepParams = OAEP.defaultOAEPParams SHA256
|
||||
|
||||
-- | RSA OAEP encryption.
|
||||
--
|
||||
-- Used as part of hybrid E2E encryption scheme and for SMP transport handshake.
|
||||
encryptOAEP :: PublicKey -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
encryptOAEP (PublicKey k) aesKey =
|
||||
liftEitherError CryptoRSAError $
|
||||
liftEitherError RSAEncryptError $
|
||||
OAEP.encrypt oaepParams k aesKey
|
||||
|
||||
decryptOAEP :: PrivateKey -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
-- | RSA OAEP decryption.
|
||||
--
|
||||
-- Used as part of hybrid E2E encryption scheme and for SMP transport handshake.
|
||||
decryptOAEP :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
decryptOAEP pk encKey =
|
||||
liftEitherError CryptoRSAError $
|
||||
liftEitherError RSADecryptError $
|
||||
OAEP.decryptSafer oaepParams (rsaPrivateKey pk) encKey
|
||||
|
||||
pssParams :: PSS.PSSParams SHA256 ByteString ByteString
|
||||
pssParams = PSS.defaultPSSParams SHA256
|
||||
|
||||
sign :: PrivateKey -> ByteString -> IO (Either R.Error Signature)
|
||||
sign pk msg = Signature <$$> PSS.signSafer pssParams (rsaPrivateKey pk) msg
|
||||
-- | RSA PSS message signing.
|
||||
--
|
||||
-- Used by SMP clients to sign SMP commands and by SMP agents to sign messages.
|
||||
sign :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO Signature
|
||||
sign pk msg = ExceptT $ bimap RSASignError Signature <$> PSS.signSafer pssParams (rsaPrivateKey pk) msg
|
||||
|
||||
-- | RSA PSS signature verification.
|
||||
--
|
||||
-- Used by SMP servers to authorize SMP commands and by SMP agents to verify messages.
|
||||
verify :: PublicKey -> Signature -> ByteString -> Bool
|
||||
verify (PublicKey k) (Signature sig) msg = PSS.verify pssParams k msg sig
|
||||
|
||||
-- | Base-64 X509 encoding of RSA public key.
|
||||
--
|
||||
-- Used as part of SMP queue information (out-of-band message).
|
||||
serializePubKey :: PublicKey -> ByteString
|
||||
serializePubKey (PublicKey k) = serializeKey_ (R.public_size k, R.public_n k, R.public_e k)
|
||||
serializePubKey = ("rsa:" <>) . encode . encodePubKey
|
||||
|
||||
serializePrivKey :: PrivateKey -> ByteString
|
||||
serializePrivKey pk = serializeKey_ (private_size pk, private_n pk, private_d pk)
|
||||
serializePubKeyUri :: PublicKey -> ByteString
|
||||
serializePubKeyUri = ("rsa:" <>) . U.encode . encodePubKey
|
||||
|
||||
serializeKey_ :: (Int, Integer, Integer) -> ByteString
|
||||
serializeKey_ (size, n, ex) = bshow size <> "," <> encInt n <> "," <> encInt ex
|
||||
where
|
||||
encInt = encode . i2osp
|
||||
-- | Base-64 PKCS8 encoding of PSA private key.
|
||||
--
|
||||
-- Not used as part of SMP protocols.
|
||||
serializePrivKey :: PrivateKey k => k -> ByteString
|
||||
serializePrivKey = ("rsa:" <>) . encode . encodePrivKey
|
||||
|
||||
-- Base-64 X509 RSA public key parser.
|
||||
pubKeyP :: Parser PublicKey
|
||||
pubKeyP = do
|
||||
(public_size, public_n, public_e) <- keyParser_
|
||||
return . PublicKey $ R.PublicKey {R.public_size, R.public_n, R.public_e}
|
||||
pubKeyP = decodePubKey <$?> ("rsa:" *> base64P)
|
||||
|
||||
privKeyP :: Parser PrivateKey
|
||||
privKeyP = do
|
||||
(private_size, private_n, private_d) <- keyParser_
|
||||
return PrivateKey {private_size, private_n, private_d}
|
||||
pubKeyUriP :: Parser PublicKey
|
||||
pubKeyUriP = decodePubKey <$?> ("rsa:" *> base64UriP)
|
||||
|
||||
parsePubKey :: ByteString -> Either String PublicKey
|
||||
parsePubKey = A.parseOnly (pubKeyP <* A.endOfInput)
|
||||
-- Binary X509 RSA public key parser.
|
||||
binaryPubKeyP :: Parser PublicKey
|
||||
binaryPubKeyP = decodePubKey <$?> A.takeByteString
|
||||
|
||||
parsePrivKey :: ByteString -> Either String PrivateKey
|
||||
parsePrivKey = A.parseOnly (privKeyP <* A.endOfInput)
|
||||
-- Base-64 PKCS8 RSA private key parser.
|
||||
privKeyP :: PrivateKey k => Parser k
|
||||
privKeyP = decodePrivKey <$?> ("rsa:" *> base64P)
|
||||
|
||||
keyParser_ :: Parser (Int, Integer, Integer)
|
||||
keyParser_ = (,,) <$> (A.decimal <* ",") <*> (intP <* ",") <*> intP
|
||||
where
|
||||
intP = os2ip <$> base64P
|
||||
-- Binary PKCS8 RSA private key parser.
|
||||
binaryPrivKeyP :: PrivateKey k => Parser k
|
||||
binaryPrivKeyP = decodePrivKey <$?> A.takeByteString
|
||||
|
||||
rsaPrivateKey :: PrivateKey -> R.PrivateKey
|
||||
rsaPrivateKey pk =
|
||||
-- | Construct 'SafePrivateKey' from three numbers - used internally and in the tests.
|
||||
safePrivateKey :: (Int, Integer, Integer) -> SafePrivateKey
|
||||
safePrivateKey = SafePrivateKey . safeRsaPrivateKey
|
||||
|
||||
safeRsaPrivateKey :: (Int, Integer, Integer) -> R.PrivateKey
|
||||
safeRsaPrivateKey (size, n, d) =
|
||||
R.PrivateKey
|
||||
{ R.private_pub =
|
||||
{ private_pub =
|
||||
R.PublicKey
|
||||
{ R.public_size = private_size pk,
|
||||
R.public_n = private_n pk,
|
||||
R.public_e = undefined
|
||||
{ public_size = size,
|
||||
public_n = n,
|
||||
public_e = 0
|
||||
},
|
||||
R.private_d = private_d pk,
|
||||
R.private_p = 0,
|
||||
R.private_q = 0,
|
||||
R.private_dP = undefined,
|
||||
R.private_dQ = undefined,
|
||||
R.private_qinv = undefined
|
||||
private_d = d,
|
||||
private_p = 0,
|
||||
private_q = 0,
|
||||
private_dP = 0,
|
||||
private_dQ = 0,
|
||||
private_qinv = 0
|
||||
}
|
||||
|
||||
-- Binary X509 encoding of 'PublicKey'.
|
||||
encodePubKey :: PublicKey -> ByteString
|
||||
encodePubKey = encodeKey . PubKeyRSA . rsaPublicKey
|
||||
|
||||
-- Binary PKCS8 encoding of 'PrivateKey'.
|
||||
encodePrivKey :: PrivateKey k => k -> ByteString
|
||||
encodePrivKey = encodeKey . PrivKeyRSA . rsaPrivateKey
|
||||
|
||||
encodeKey :: ASN1Object a => a -> ByteString
|
||||
encodeKey k = toStrict . encodeASN1 DER $ toASN1 k []
|
||||
|
||||
-- Decoding of binary X509 'PublicKey'.
|
||||
decodePubKey :: ByteString -> Either String PublicKey
|
||||
decodePubKey =
|
||||
decodeKey >=> \case
|
||||
(PubKeyRSA k, []) -> Right $ PublicKey k
|
||||
r -> keyError r
|
||||
|
||||
-- Decoding of binary PKCS8 'PrivateKey'.
|
||||
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"
|
||||
|
||||
@@ -1,23 +1,80 @@
|
||||
{-# 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 qualified Data.ByteString.Base64.URL as U
|
||||
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 = do
|
||||
str <- A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
|
||||
pad <- A.takeWhile (== '=')
|
||||
either fail pure $ decode (str <> pad)
|
||||
base64P = decode <$?> base64StringP
|
||||
|
||||
base64StringP :: Parser ByteString
|
||||
base64StringP = paddedBase64 rawBase64P
|
||||
|
||||
base64UriP :: Parser ByteString
|
||||
base64UriP = U.decode <$?> base64UriStringP
|
||||
|
||||
base64UriStringP :: Parser ByteString
|
||||
base64UriStringP = paddedBase64 rawBase64UriP
|
||||
|
||||
paddedBase64 :: Parser ByteString -> Parser ByteString
|
||||
paddedBase64 raw = (<>) <$> raw <*> pad
|
||||
where
|
||||
pad = A.takeWhile (== '=')
|
||||
|
||||
rawBase64P :: Parser ByteString
|
||||
rawBase64P = A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
|
||||
|
||||
rawBase64UriP :: Parser ByteString
|
||||
rawBase64UriP = A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_')
|
||||
|
||||
tsISO8601P :: Parser UTCTime
|
||||
tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill (== ' ')
|
||||
tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill wordEnd
|
||||
|
||||
parse :: Parser a -> e -> (ByteString -> Either e a)
|
||||
parse parser err = first (const err) . A.parseOnly (parser <* A.endOfInput)
|
||||
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 wordEnd
|
||||
|
||||
parseRead2 :: Read a => Parser a
|
||||
parseRead2 = parseRead $ do
|
||||
w1 <- A.takeTill wordEnd <* A.char ' '
|
||||
w2 <- A.takeTill wordEnd
|
||||
pure $ w1 <> " " <> w2
|
||||
|
||||
wordEnd :: Char -> Bool
|
||||
wordEnd c = c == ' ' || c == '\n'
|
||||
|
||||
parseString :: (ByteString -> Either String a) -> (String -> a)
|
||||
parseString p = either error id . p . B.pack
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
@@ -9,11 +10,62 @@
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Protocol where
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Protocol
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- Types, parsers, serializers and functions to send and receive SMP protocol commands and responses.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
|
||||
module Simplex.Messaging.Protocol
|
||||
( -- * SMP protocol types
|
||||
Command (..),
|
||||
Party (..),
|
||||
Cmd (..),
|
||||
SParty (..),
|
||||
ErrorType (..),
|
||||
CommandError (..),
|
||||
Transmission,
|
||||
SignedTransmission,
|
||||
SignedTransmissionOrError,
|
||||
RawTransmission,
|
||||
SignedRawTransmission,
|
||||
CorrId (..),
|
||||
QueueId,
|
||||
RecipientId,
|
||||
SenderId,
|
||||
RecipientPrivateKey,
|
||||
RecipientPublicKey,
|
||||
SenderPrivateKey,
|
||||
SenderPublicKey,
|
||||
Encoded,
|
||||
MsgId,
|
||||
MsgBody,
|
||||
|
||||
-- * Parse and serialize
|
||||
serializeTransmission,
|
||||
serializeCommand,
|
||||
serializeErrorType,
|
||||
transmissionP,
|
||||
commandP,
|
||||
errorTypeP,
|
||||
|
||||
-- * TCP transport functions
|
||||
tPut,
|
||||
tGet,
|
||||
fromClient,
|
||||
fromServer,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Except
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Base64
|
||||
@@ -24,16 +76,19 @@ 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.Transport (THandle, Transport, TransportError (..), tGetEncrypted, tPutEncrypted)
|
||||
import Simplex.Messaging.Util
|
||||
import System.IO
|
||||
import Text.Read
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
|
||||
-- | SMP protocol participants.
|
||||
data Party = Broker | Recipient | Sender
|
||||
deriving (Show)
|
||||
|
||||
-- | Singleton types for SMP protocol participants.
|
||||
data SParty :: Party -> Type where
|
||||
SBroker :: SParty Broker
|
||||
SRecipient :: SParty Recipient
|
||||
@@ -41,28 +96,38 @@ data SParty :: Party -> Type where
|
||||
|
||||
deriving instance Show (SParty a)
|
||||
|
||||
-- | Type for command or response of any participant.
|
||||
data Cmd = forall a. Cmd (SParty a) (Command a)
|
||||
|
||||
deriving instance Show Cmd
|
||||
|
||||
-- | SMP transmission without signature.
|
||||
type Transmission = (CorrId, QueueId, Cmd)
|
||||
|
||||
-- | SMP transmission with signature.
|
||||
type SignedTransmission = (C.Signature, Transmission)
|
||||
|
||||
type TransmissionOrError = (CorrId, QueueId, Either ErrorType Cmd)
|
||||
|
||||
-- | signed parsed transmission, with parsing error.
|
||||
type SignedTransmissionOrError = (C.Signature, TransmissionOrError)
|
||||
|
||||
-- | unparsed SMP transmission with signature.
|
||||
type RawTransmission = (ByteString, ByteString, ByteString, ByteString)
|
||||
|
||||
-- | unparsed SMP transmission with signature.
|
||||
type SignedRawTransmission = (C.Signature, ByteString)
|
||||
|
||||
-- | SMP queue ID for the recipient.
|
||||
type RecipientId = QueueId
|
||||
|
||||
-- | SMP queue ID for the sender.
|
||||
type SenderId = QueueId
|
||||
|
||||
-- | SMP queue ID on the server.
|
||||
type QueueId = Encoded
|
||||
|
||||
-- | Parameterized type for SMP protocol commands from all participants.
|
||||
data Command (a :: Party) where
|
||||
-- SMP recipient commands
|
||||
NEW :: RecipientPublicKey -> Command Recipient
|
||||
@@ -86,48 +151,89 @@ deriving instance Show (Command a)
|
||||
|
||||
deriving instance Eq (Command a)
|
||||
|
||||
-- | Base-64 encoded string.
|
||||
type Encoded = ByteString
|
||||
|
||||
-- newtype to avoid accidentally changing order of transmission parts
|
||||
-- | Transmission correlation ID.
|
||||
--
|
||||
-- A 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.PrivateKey
|
||||
-- | Recipient's private key used by the recipient to authorize (sign) SMP commands.
|
||||
--
|
||||
-- Only used by SMP agent, kept here so its definition is close to respective public key.
|
||||
type RecipientPrivateKey = C.SafePrivateKey
|
||||
|
||||
-- | Recipient's public key used by SMP server to verify authorization of SMP commands.
|
||||
type RecipientPublicKey = C.PublicKey
|
||||
|
||||
-- only used by Agent, kept here so its definition is close to respective public key
|
||||
type SenderPrivateKey = C.PrivateKey
|
||||
-- | Sender's private key used by the recipient to authorize (sign) SMP commands.
|
||||
--
|
||||
-- Only used by SMP agent, kept here so its definition is close to respective public key.
|
||||
type SenderPrivateKey = C.SafePrivateKey
|
||||
|
||||
-- | Sender's public key used by SMP server to verify authorization of SMP commands.
|
||||
type SenderPublicKey = C.PublicKey
|
||||
|
||||
-- | SMP message server ID.
|
||||
type MsgId = Encoded
|
||||
|
||||
-- | SMP message body.
|
||||
type MsgBody = ByteString
|
||||
|
||||
data ErrorType = PROHIBITED | SYNTAX Int | SIZE | AUTH | INTERNAL | DUPLICATE deriving (Show, Eq)
|
||||
-- | Type for protocol errors.
|
||||
data ErrorType
|
||||
= -- | incorrect block format, encoding or signature size
|
||||
BLOCK
|
||||
| -- | SMP command is unknown or has invalid syntax
|
||||
CMD CommandError
|
||||
| -- | command authorization error - bad signature or non-existing SMP queue
|
||||
AUTH
|
||||
| -- | SMP queue capacity is exceeded on the server
|
||||
QUOTA
|
||||
| -- | ACK command is sent without message to be acknowledged
|
||||
NO_MSG
|
||||
| -- | internal server error
|
||||
INTERNAL
|
||||
| -- | used internally, never returned by the server (to be removed)
|
||||
DUPLICATE_ -- TODO remove, not part of SMP protocol
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
errBadTransmission :: Int
|
||||
errBadTransmission = 1
|
||||
-- | SMP command error type.
|
||||
data CommandError
|
||||
= -- | server response sent from client or vice versa
|
||||
PROHIBITED
|
||||
| -- | bad RSA key size in NEW or KEY commands (only 1024, 2048 and 4096 bits keys are allowed)
|
||||
KEY_SIZE
|
||||
| -- | error parsing command
|
||||
SYNTAX
|
||||
| -- | transmission has no required credentials (signature or queue ID)
|
||||
NO_AUTH
|
||||
| -- | transmission has credentials that are not allowed for this command
|
||||
HAS_AUTH
|
||||
| -- | transmission has no required queue ID
|
||||
NO_QUEUE
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
errBadSMPCommand :: Int
|
||||
errBadSMPCommand = 2
|
||||
instance Arbitrary ErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
errNoCredentials :: Int
|
||||
errNoCredentials = 3
|
||||
instance Arbitrary CommandError where arbitrary = genericArbitraryU
|
||||
|
||||
errHasCredentials :: Int
|
||||
errHasCredentials = 4
|
||||
|
||||
errNoQueueId :: Int
|
||||
errNoQueueId = 5
|
||||
|
||||
errMessageBody :: Int
|
||||
errMessageBody = 6
|
||||
-- | SMP transmission parser.
|
||||
transmissionP :: Parser RawTransmission
|
||||
transmissionP = do
|
||||
signature <- segment
|
||||
corrId <- segment
|
||||
queueId <- segment
|
||||
command <- A.takeByteString
|
||||
return (signature, corrId, queueId, command)
|
||||
where
|
||||
segment = A.takeTill (== ' ') <* " "
|
||||
|
||||
-- | SMP command parser.
|
||||
commandP :: Parser Cmd
|
||||
commandP =
|
||||
"NEW " *> newCmd
|
||||
@@ -148,132 +254,127 @@ commandP =
|
||||
newCmd = Cmd SRecipient . NEW <$> C.pubKeyP
|
||||
idsResp = Cmd SBroker <$> (IDS <$> (base64P <* A.space) <*> base64P)
|
||||
keyCmd = Cmd SRecipient . KEY <$> C.pubKeyP
|
||||
sendCmd = Cmd SSender . SEND <$> A.takeWhile A.isDigit
|
||||
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
|
||||
Cmd SBroker . MSG msgId ts <$> A.takeWhile A.isDigit
|
||||
serverError = Cmd SBroker . ERR <$> errorType
|
||||
errorType =
|
||||
"PROHIBITED" $> PROHIBITED
|
||||
<|> "SYNTAX " *> (SYNTAX <$> A.decimal)
|
||||
<|> "SIZE" $> SIZE
|
||||
<|> "AUTH" $> AUTH
|
||||
<|> "INTERNAL" $> INTERNAL
|
||||
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
|
||||
|
||||
-- | Parse SMP command.
|
||||
parseCommand :: ByteString -> Either ErrorType Cmd
|
||||
parseCommand = parse commandP $ SYNTAX errBadSMPCommand
|
||||
parseCommand = parse (commandP <* " " <* A.takeByteString) $ CMD SYNTAX
|
||||
|
||||
-- | Serialize SMP command.
|
||||
serializeCommand :: Cmd -> ByteString
|
||||
serializeCommand = \case
|
||||
Cmd SRecipient (NEW rKey) -> "NEW " <> C.serializePubKey rKey
|
||||
Cmd SRecipient (KEY sKey) -> "KEY " <> C.serializePubKey sKey
|
||||
Cmd SRecipient cmd -> B.pack $ show cmd
|
||||
Cmd SSender (SEND msgBody) -> "SEND" <> serializeMsg msgBody
|
||||
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
|
||||
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
|
||||
Cmd SBroker (ERR err) -> "ERR " <> serializeErrorType err
|
||||
Cmd SBroker resp -> bshow resp
|
||||
where
|
||||
serializeMsg msgBody = " " <> B.pack (show $ B.length msgBody) <> "\r\n" <> msgBody
|
||||
serializeMsg msgBody = bshow (B.length msgBody) <> " " <> msgBody <> " "
|
||||
|
||||
tPutRaw :: Handle -> RawTransmission -> IO ()
|
||||
tPutRaw h (signature, corrId, queueId, command) = do
|
||||
putLn h signature
|
||||
putLn h corrId
|
||||
putLn h queueId
|
||||
putLn h command
|
||||
-- | SMP error parser.
|
||||
errorTypeP :: Parser ErrorType
|
||||
errorTypeP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
|
||||
|
||||
tGetRaw :: Handle -> IO RawTransmission
|
||||
tGetRaw h = do
|
||||
signature <- getLn h
|
||||
corrId <- getLn h
|
||||
queueId <- getLn h
|
||||
command <- getLn h
|
||||
return (signature, corrId, queueId, command)
|
||||
-- | Serialize SMP error.
|
||||
serializeErrorType :: ErrorType -> ByteString
|
||||
serializeErrorType = bshow
|
||||
|
||||
tPut :: Handle -> SignedRawTransmission -> IO ()
|
||||
tPut h (C.Signature sig, t) = do
|
||||
putLn h $ encode sig
|
||||
putLn h t
|
||||
-- | Send signed SMP transmission to TCP transport.
|
||||
tPut :: Transport c => THandle c -> SignedRawTransmission -> IO (Either TransportError ())
|
||||
tPut th (C.Signature sig, t) =
|
||||
tPutEncrypted th $ encode sig <> " " <> t <> " "
|
||||
|
||||
-- | Serialize SMP transmission.
|
||||
serializeTransmission :: Transmission -> ByteString
|
||||
serializeTransmission (CorrId corrId, queueId, command) =
|
||||
B.intercalate "\r\n" [corrId, encode queueId, serializeCommand command]
|
||||
B.intercalate " " [corrId, encode queueId, serializeCommand command]
|
||||
|
||||
-- | Validate that it is an SMP client command, used with 'tGet' by 'Simplex.Messaging.Server'.
|
||||
fromClient :: Cmd -> Either ErrorType Cmd
|
||||
fromClient = \case
|
||||
Cmd SBroker _ -> Left PROHIBITED
|
||||
Cmd SBroker _ -> Left $ CMD PROHIBITED
|
||||
cmd -> Right cmd
|
||||
|
||||
-- | Validate that it is an SMP server command, used with 'tGet' by 'Simplex.Messaging.Client'.
|
||||
fromServer :: Cmd -> Either ErrorType Cmd
|
||||
fromServer = \case
|
||||
cmd@(Cmd SBroker _) -> Right cmd
|
||||
_ -> Left PROHIBITED
|
||||
_ -> Left $ CMD 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 SignedTransmissionOrError
|
||||
tGet fromParty h = do
|
||||
(signature, corrId, queueId, command) <- liftIO $ tGetRaw h
|
||||
let decodedTransmission = liftM2 (,corrId,,command) (decode signature) (decode queueId)
|
||||
either (const $ tError corrId) tParseLoadBody decodedTransmission
|
||||
-- | Receive and parse transmission from the TCP transport.
|
||||
tGetParse :: Transport c => THandle c -> IO (Either TransportError RawTransmission)
|
||||
tGetParse th = (>>= parse transmissionP TEBadBlock) <$> tGetEncrypted th
|
||||
|
||||
-- | Receive client and server transmissions.
|
||||
--
|
||||
-- The first argument is used to limit allowed senders.
|
||||
-- 'fromClient' or 'fromServer' should be used here.
|
||||
tGet :: forall c m. (Transport c, MonadIO m) => (Cmd -> Either ErrorType Cmd) -> THandle c -> m SignedTransmissionOrError
|
||||
tGet fromParty th = liftIO (tGetParse th) >>= decodeParseValidate
|
||||
where
|
||||
tError :: ByteString -> m SignedTransmissionOrError
|
||||
tError corrId = return (C.Signature B.empty, (CorrId corrId, B.empty, Left $ SYNTAX errBadTransmission))
|
||||
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 ""
|
||||
|
||||
tParseLoadBody :: RawTransmission -> m SignedTransmissionOrError
|
||||
tParseLoadBody t@(sig, corrId, queueId, command) = do
|
||||
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
|
||||
fullCmd <- either (return . Left) cmdWithMsgBody cmd
|
||||
return (C.Signature sig, (CorrId corrId, queueId, fullCmd))
|
||||
return (C.Signature sig, (CorrId corrId, queueId, cmd))
|
||||
|
||||
tCredentials :: RawTransmission -> Cmd -> Either ErrorType Cmd
|
||||
tCredentials (signature, _, queueId, _) cmd = case cmd of
|
||||
-- IDS response should not have queue ID
|
||||
-- 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 should not have queue ID
|
||||
-- PONG response must not have queue ID
|
||||
Cmd SBroker PONG
|
||||
| B.null queueId -> Right cmd
|
||||
| otherwise -> Left $ SYNTAX errHasCredentials
|
||||
| otherwise -> Left $ CMD HAS_AUTH
|
||||
-- other responses must have queue ID
|
||||
Cmd SBroker _
|
||||
| B.null queueId -> Left $ SYNTAX errNoQueueId
|
||||
| B.null queueId -> Left $ CMD NO_QUEUE
|
||||
| otherwise -> Right cmd
|
||||
-- NEW must NOT have signature or queue ID
|
||||
-- NEW must have signature but NOT queue ID
|
||||
Cmd SRecipient (NEW _)
|
||||
| B.null signature -> Left $ SYNTAX errNoCredentials
|
||||
| not (B.null queueId) -> Left $ SYNTAX errHasCredentials
|
||||
| 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 $ SYNTAX errNoQueueId
|
||||
| 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 $ SYNTAX errHasCredentials
|
||||
| otherwise -> Left $ CMD HAS_AUTH
|
||||
-- other client commands must have both signature and queue ID
|
||||
Cmd SRecipient _
|
||||
| B.null signature || B.null queueId -> Left $ SYNTAX errNoCredentials
|
||||
| B.null signature || B.null queueId -> Left $ CMD NO_AUTH
|
||||
| otherwise -> Right cmd
|
||||
|
||||
cmdWithMsgBody :: Cmd -> m (Either ErrorType Cmd)
|
||||
cmdWithMsgBody = \case
|
||||
Cmd SSender (SEND sizeStr) ->
|
||||
Cmd SSender . SEND <$$> getMsgBody sizeStr
|
||||
Cmd SBroker (MSG msgId ts sizeStr) ->
|
||||
Cmd SBroker . MSG msgId ts <$$> getMsgBody sizeStr
|
||||
cmd -> return $ Right cmd
|
||||
|
||||
getMsgBody :: MsgBody -> m (Either ErrorType MsgBody)
|
||||
getMsgBody sizeStr = case readMaybe (B.unpack sizeStr) :: 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 SIZE
|
||||
Nothing -> return $ Left INTERNAL
|
||||
|
||||
@@ -10,11 +10,24 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
-- TODO move randomBytes to another module
|
||||
module Simplex.Messaging.Server (runSMPServer, randomBytes) where
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Server
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- This module defines SMP protocol server with in-memory persistence
|
||||
-- and optional append only log of SMP queue records.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
|
||||
module Simplex.Messaging.Server (runSMPServer, runSMPServerBlocking) 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
|
||||
@@ -23,6 +36,7 @@ import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Time.Clock
|
||||
import Network.Socket (ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
@@ -30,52 +44,96 @@ 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
|
||||
|
||||
-- | Runs an SMP server using passed configuration.
|
||||
--
|
||||
-- See a full server here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs
|
||||
runSMPServer :: (MonadRandom m, MonadUnliftIO m) => ServerConfig -> m ()
|
||||
runSMPServer cfg@ServerConfig {tcpPort} = do
|
||||
runSMPServer cfg = do
|
||||
started <- newEmptyTMVarIO
|
||||
runSMPServerBlocking started cfg
|
||||
|
||||
-- | Runs an SMP server using passed configuration with signalling.
|
||||
--
|
||||
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
|
||||
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
|
||||
runSMPServerBlocking :: (MonadRandom m, MonadUnliftIO m) => TMVar Bool -> ServerConfig -> m ()
|
||||
runSMPServerBlocking started cfg@ServerConfig {transports} = do
|
||||
env <- newEnv cfg
|
||||
runReaderT smpServer env
|
||||
where
|
||||
smpServer :: (MonadUnliftIO m, MonadReader Env m) => m ()
|
||||
smpServer :: (MonadUnliftIO m', MonadReader Env m') => m' ()
|
||||
smpServer = do
|
||||
s <- asks server
|
||||
race_ (runTCPServer tcpPort runClient) (serverThread s)
|
||||
raceAny_ (serverThread s : map runServer transports)
|
||||
`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
|
||||
runServer :: (MonadUnliftIO m', MonadReader Env m') => (ServiceName, ATransport) -> m' ()
|
||||
runServer (tcpPort, ATransport t) = runTransportServer started tcpPort (runClient t)
|
||||
|
||||
runClient :: (MonadUnliftIO m, MonadReader Env m) => Handle -> m ()
|
||||
runClient h = do
|
||||
liftIO $ putLn h "Welcome to SMP v0.2.0"
|
||||
serverThread :: MonadUnliftIO m' => Server -> m' ()
|
||||
serverThread Server {subscribedQ, subscribers} = forever $ do
|
||||
atomically updateSubscribers >>= \case
|
||||
Just (rId, Client {rcvQ}) ->
|
||||
void . forkIO . atomically $
|
||||
writeTBQueue rcvQ (CorrId "", rId, Cmd SBroker END)
|
||||
_ -> pure ()
|
||||
where
|
||||
updateSubscribers :: STM (Maybe (RecipientId, Client))
|
||||
updateSubscribers = do
|
||||
(rId, c) <- readTBQueue subscribedQ
|
||||
stateTVar subscribers (\cs -> (M.lookup rId cs, M.insert rId c cs)) >>= \case
|
||||
Just c' -> clientToBeNotified rId c c'
|
||||
_ -> pure Nothing
|
||||
clientToBeNotified :: RecipientId -> Client -> Client -> STM (Maybe (RecipientId, Client))
|
||||
clientToBeNotified rId c c'@Client {connected}
|
||||
| clientId c /= clientId c' = do
|
||||
yes <- readTVar connected
|
||||
pure $ if yes then Just (rId, c') else Nothing
|
||||
| otherwise = pure Nothing
|
||||
|
||||
runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m ()
|
||||
runClient _ h = do
|
||||
keyPair <- asks serverKeyPair
|
||||
ServerConfig {blockSize} <- asks config
|
||||
liftIO (runExceptT $ serverHandshake h blockSize keyPair) >>= \case
|
||||
Right th -> runClientTransport th
|
||||
Left _ -> pure ()
|
||||
|
||||
runClientTransport :: (Transport c, MonadUnliftIO m, MonadReader Env m) => THandle c -> m ()
|
||||
runClientTransport th = do
|
||||
q <- asks $ tbqSize . config
|
||||
c <- atomically $ newClient q
|
||||
s <- asks server
|
||||
raceAny_ [send h c, client c s, receive h c]
|
||||
`finally` cancelSubscribers c
|
||||
c <- atomically $ newClient s q
|
||||
raceAny_ [send th c, client c s, receive th c]
|
||||
`finally` clientDisconnected c
|
||||
|
||||
cancelSubscribers :: MonadUnliftIO m => Client -> m ()
|
||||
cancelSubscribers Client {subscriptions} =
|
||||
readTVarIO subscriptions >>= mapM_ cancelSub
|
||||
clientDisconnected :: (MonadUnliftIO m, MonadReader Env m) => Client -> m ()
|
||||
clientDisconnected c@Client {subscriptions, connected} = do
|
||||
atomically $ writeTVar connected False
|
||||
subs <- readTVarIO subscriptions
|
||||
mapM_ cancelSub subs
|
||||
cs <- asks $ subscribers . server
|
||||
atomically . mapM_ (modifyTVar cs . M.update deleteCurrentClient) $ M.keys subs
|
||||
where
|
||||
deleteCurrentClient :: Client -> Maybe Client
|
||||
deleteCurrentClient c'
|
||||
| clientId c == clientId c' = Nothing
|
||||
| otherwise = Just c'
|
||||
|
||||
cancelSub :: MonadUnliftIO m => Sub -> m ()
|
||||
cancelSub = \case
|
||||
Sub {subThread = SubThread t} -> killThread t
|
||||
_ -> return ()
|
||||
|
||||
receive :: (MonadUnliftIO m, MonadReader Env m) => Handle -> Client -> m ()
|
||||
receive :: (Transport c, MonadUnliftIO m, MonadReader Env m) => THandle c -> Client -> m ()
|
||||
receive h Client {rcvQ} = forever $ do
|
||||
(signature, (corrId, queueId, cmdOrError)) <- tGet fromClient h
|
||||
t <- case cmdOrError of
|
||||
@@ -83,7 +141,7 @@ receive h Client {rcvQ} = forever $ do
|
||||
Right cmd -> verifyTransmission (signature, (corrId, queueId, cmd))
|
||||
atomically $ writeTBQueue rcvQ t
|
||||
|
||||
send :: MonadUnliftIO m => Handle -> Client -> m ()
|
||||
send :: (Transport c, MonadUnliftIO m) => THandle c -> Client -> m ()
|
||||
send h Client {sndQ} = forever $ do
|
||||
t <- atomically $ readTBQueue sndQ
|
||||
liftIO $ tPut h ("", serializeTransmission t)
|
||||
@@ -95,27 +153,50 @@ verifyTransmission :: forall m. (MonadUnliftIO m, MonadReader Env m) => SignedTr
|
||||
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) -> return $ verifySignature k
|
||||
Cmd SRecipient _ -> withQueueRec SRecipient $ verifySignature . recipientKey
|
||||
Cmd SSender (SEND _) -> withQueueRec SSender $ verifySend sig . senderKey
|
||||
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
|
||||
withQueueRec :: SParty (p :: Party) -> (QueueRec -> Cmd) -> m Cmd
|
||||
withQueueRec party f = do
|
||||
verifyCmd :: SParty p -> (QueueRec -> Cmd) -> m Cmd
|
||||
verifyCmd party f = do
|
||||
st <- asks queueStore
|
||||
qr <- atomically $ getQueue st party queueId
|
||||
return $ either smpErr f qr
|
||||
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 C.verify key sig (serializeTransmission t)
|
||||
then cmd
|
||||
else authErr
|
||||
|
||||
smpErr e = Cmd SBroker $ ERR e
|
||||
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
|
||||
384 -> dummyKey384
|
||||
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"
|
||||
|
||||
dummyKey384 :: C.PublicKey
|
||||
dummyKey384 = "MIICITANBgkqhkiG9w0BAQEFAAOCAg4AMIICCQKCAYEAthExp77lSFBMB0RedjgKIU+oNH5lMGdMqDCG0E5Ly7X49rFpfDMMN08GDIgvzg9kcwV3ScbPcjUE19wmAShX9f9k3w38KM3wmIBKSiuCREQl0V3xAYp1SYwiAkMNSSwxuIkDEeSOR56WdEcZvqbB4lY9MQlUv70KriPDxZaqKCTKslUezXHQuYPQX6eMnGFK7hxz5Kl5MajV52d+5iXsa8CA+m/e1KVnbelCO+xhN89xG8ALt0CJ9k5Wwo3myLgXi4dmNankCmg8jkh+7y2ywkzxMwH1JydDtV/FLzkbZsbPR2w93TNrTq1RJOuqMyh0VtdBSpxNW/Ft988TkkX2BAWzx82INw7W6/QbHGNtHNB995R4sgeYy8QbEpNGBhQnfQh7yRWygLTVXWKApQzzfCeIoDDWUS7dMv/zXoasAnpDBj+6UhHv3BHrps7kBvRyZQ2d/nUuAqiGd43ljJ++n6vNyFLgZoiV7HLia/FOGMkdt7j92CNmFHxiT6Xl7kRHAoGBAPNoWny2O7LBxzAKMLmQVHBAiKp6RMx+7URvtQDHDHPaZ7F3MvtvmYWwGzund3cQFAaV1EkJoYeI3YRuj6xdXgMyMaP54On++btArb6jUtZuvlC98qE8dEEHQNh+7TsCiMU+ivbeKFxS9A/B7OVedoMnPoJWhatbA9zB/6L1GNPh"
|
||||
|
||||
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} =
|
||||
@@ -137,33 +218,58 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
NEW rKey -> createQueue st rKey
|
||||
SUB -> subscribeQueue queueId
|
||||
ACK -> acknowledgeMsg
|
||||
KEY sKey -> okResp <$> atomically (secureQueue st queueId sKey)
|
||||
OFF -> okResp <$> atomically (suspendQueue st queueId)
|
||||
KEY sKey -> secureQueue_ st sKey
|
||||
OFF -> suspendQueue_ st
|
||||
DEL -> delQueueAndMsgs st
|
||||
where
|
||||
createQueue :: QueueStore -> RecipientPublicKey -> m Transmission
|
||||
createQueue st rKey =
|
||||
mkResp corrId B.empty <$> addSubscribe
|
||||
checkKeySize rKey addSubscribe
|
||||
where
|
||||
addSubscribe =
|
||||
addQueueRetry 3 >>= \case
|
||||
Left e -> return $ ERR e
|
||||
Right (rId, sId) -> subscribeQueue rId $> IDS rId sId
|
||||
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 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
|
||||
@@ -190,7 +296,7 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
atomically (withSub queueId $ \s -> const s <$$> tryTakeTMVar (delivered s))
|
||||
>>= \case
|
||||
Just (Just s) -> deliverMessage tryDelPeekMsg queueId s
|
||||
_ -> return $ err PROHIBITED
|
||||
_ -> return $ err NO_MSG
|
||||
|
||||
withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a)
|
||||
withSub rId f = readTVar subscriptions >>= mapM f . M.lookup rId
|
||||
@@ -212,16 +318,19 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
QueueActive -> do
|
||||
ms <- asks msgStore
|
||||
msg <- mkMessage
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
atomically $ do
|
||||
q <- getMsgQueue ms (recipientId qr)
|
||||
writeMsg q msg
|
||||
return ok
|
||||
q <- getMsgQueue ms (recipientId qr) quota
|
||||
isFull q >>= \case
|
||||
False -> writeMsg q msg $> ok
|
||||
True -> pure $ err QUOTA
|
||||
|
||||
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
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
q <- atomically $ getMsgQueue ms rId quota
|
||||
atomically (tryPeek q) >>= \case
|
||||
Nothing -> forkSub q $> ok
|
||||
Just msg -> atomically setDelivered $> mkResp corrId rId (msgCmd msg)
|
||||
@@ -238,7 +347,7 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
subscriber :: MsgQueue -> m ()
|
||||
subscriber q = atomically $ do
|
||||
msg <- peekMsg q
|
||||
writeTBQueue sndQ $ mkResp (CorrId B.empty) rId (msgCmd msg)
|
||||
writeTBQueue sndQ $ mkResp (CorrId "") rId (msgCmd msg)
|
||||
setSub (\s -> s {subThread = NoSub})
|
||||
void setDelivered
|
||||
|
||||
@@ -250,6 +359,7 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
|
||||
delQueueAndMsgs :: QueueStore -> m Transmission
|
||||
delQueueAndMsgs st = do
|
||||
withLog (`logDeleteQueue` queueId)
|
||||
ms <- asks msgStore
|
||||
atomically $
|
||||
deleteQueue st queueId >>= \case
|
||||
@@ -268,6 +378,11 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
|
||||
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
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Server.Env.STM where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
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 Simplex.Messaging.Transport (ATransport)
|
||||
import System.IO (IOMode (..))
|
||||
import UnliftIO.STM
|
||||
|
||||
data ServerConfig = ServerConfig
|
||||
{ tcpPort :: ServiceName,
|
||||
{ transports :: [(ServiceName, ATransport)],
|
||||
tbqSize :: Natural,
|
||||
serverTbqSize :: Natural,
|
||||
msgQueueQuota :: Natural,
|
||||
queueIdBytes :: Int,
|
||||
msgIdBytes :: Int
|
||||
msgIdBytes :: Int,
|
||||
storeLog :: Maybe (StoreLog 'ReadMode),
|
||||
blockSize :: Int,
|
||||
serverPrivateKey :: C.FullPrivateKey
|
||||
-- serverId :: ByteString
|
||||
}
|
||||
|
||||
data Env = Env
|
||||
@@ -27,18 +41,23 @@ data Env = Env
|
||||
server :: Server,
|
||||
queueStore :: QueueStore,
|
||||
msgStore :: STMMsgStore,
|
||||
idsDrg :: TVar ChaChaDRG
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
serverKeyPair :: C.FullKeyPair,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode)
|
||||
}
|
||||
|
||||
data Server = Server
|
||||
{ subscribedQ :: TBQueue (RecipientId, Client),
|
||||
subscribers :: TVar (Map RecipientId Client)
|
||||
subscribers :: TVar (Map RecipientId Client),
|
||||
nextClientId :: TVar Natural
|
||||
}
|
||||
|
||||
data Client = Client
|
||||
{ subscriptions :: TVar (Map RecipientId Sub),
|
||||
rcvQ :: TBQueue Transmission,
|
||||
sndQ :: TBQueue Transmission
|
||||
sndQ :: TBQueue Transmission,
|
||||
clientId :: Natural,
|
||||
connected :: TVar Bool
|
||||
}
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread ThreadId
|
||||
@@ -52,24 +71,38 @@ newServer :: Natural -> STM Server
|
||||
newServer qSize = do
|
||||
subscribedQ <- newTBQueue qSize
|
||||
subscribers <- newTVar M.empty
|
||||
return Server {subscribedQ, subscribers}
|
||||
nextClientId <- newTVar 0
|
||||
return Server {subscribedQ, subscribers, nextClientId}
|
||||
|
||||
newClient :: Natural -> STM Client
|
||||
newClient qSize = do
|
||||
newClient :: Server -> Natural -> STM Client
|
||||
newClient Server {nextClientId} qSize = do
|
||||
subscriptions <- newTVar M.empty
|
||||
rcvQ <- newTBQueue qSize
|
||||
sndQ <- newTBQueue qSize
|
||||
return Client {subscriptions, rcvQ, sndQ}
|
||||
clientId <- stateTVar nextClientId $ \i -> (i, i + 1)
|
||||
connected <- newTVar True
|
||||
return Client {subscriptions, rcvQ, sndQ, clientId, connected}
|
||||
|
||||
newSubscription :: STM Sub
|
||||
newSubscription = do
|
||||
delivered <- newEmptyTMVar
|
||||
return Sub {subThread = NoSub, delivered}
|
||||
|
||||
newEnv :: (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
|
||||
newEnv :: forall m. (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
|
||||
newEnv config = do
|
||||
server <- atomically $ newServer (tbqSize config)
|
||||
server <- atomically $ newServer (serverTbqSize config)
|
||||
queueStore <- atomically newQueueStore
|
||||
msgStore <- atomically newMsgStore
|
||||
idsDrg <- drgNew >>= newTVarIO
|
||||
return Env {config, server, queueStore, msgStore, idsDrg}
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
module Simplex.Messaging.Server.MsgStore where
|
||||
|
||||
import Data.Time.Clock
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Protocol (Encoded, MsgBody, RecipientId)
|
||||
|
||||
data Message = Message
|
||||
@@ -12,10 +13,11 @@ data Message = Message
|
||||
}
|
||||
|
||||
class MonadMsgStore s q m | s -> q where
|
||||
getMsgQueue :: s -> RecipientId -> m q
|
||||
getMsgQueue :: s -> RecipientId -> Natural -> m q
|
||||
delMsgQueue :: s -> RecipientId -> m ()
|
||||
|
||||
class MonadMsgQueue q m where
|
||||
isFull :: q -> m Bool
|
||||
writeMsg :: q -> Message -> m () -- non blocking
|
||||
tryPeekMsg :: q -> m (Maybe Message) -- non blocking
|
||||
peekMsg :: q -> m Message -- blocking
|
||||
|
||||
@@ -8,11 +8,12 @@ module Simplex.Messaging.Server.MsgStore.STM where
|
||||
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Protocol (RecipientId)
|
||||
import Simplex.Messaging.Server.MsgStore
|
||||
import UnliftIO.STM
|
||||
|
||||
newtype MsgQueue = MsgQueue {msgQueue :: TQueue Message}
|
||||
newtype MsgQueue = MsgQueue {msgQueue :: TBQueue Message}
|
||||
|
||||
newtype MsgStoreData = MsgStoreData {messages :: Map RecipientId MsgQueue}
|
||||
|
||||
@@ -22,13 +23,13 @@ newMsgStore :: STM STMMsgStore
|
||||
newMsgStore = newTVar $ MsgStoreData M.empty
|
||||
|
||||
instance MonadMsgStore STMMsgStore MsgQueue STM where
|
||||
getMsgQueue :: STMMsgStore -> RecipientId -> STM MsgQueue
|
||||
getMsgQueue store rId = do
|
||||
getMsgQueue :: STMMsgStore -> RecipientId -> Natural -> STM MsgQueue
|
||||
getMsgQueue store rId quota = do
|
||||
m <- messages <$> readTVar store
|
||||
maybe (newQ m) return $ M.lookup rId m
|
||||
where
|
||||
newQ m' = do
|
||||
q <- MsgQueue <$> newTQueue
|
||||
q <- MsgQueue <$> newTBQueue quota
|
||||
writeTVar store . MsgStoreData $ M.insert rId q m'
|
||||
return q
|
||||
|
||||
@@ -37,15 +38,18 @@ instance MonadMsgStore STMMsgStore MsgQueue STM where
|
||||
modifyTVar store $ MsgStoreData . M.delete rId . messages
|
||||
|
||||
instance MonadMsgQueue MsgQueue STM where
|
||||
isFull :: MsgQueue -> STM Bool
|
||||
isFull = isFullTBQueue . msgQueue
|
||||
|
||||
writeMsg :: MsgQueue -> Message -> STM ()
|
||||
writeMsg = writeTQueue . msgQueue
|
||||
writeMsg = writeTBQueue . msgQueue
|
||||
|
||||
tryPeekMsg :: MsgQueue -> STM (Maybe Message)
|
||||
tryPeekMsg = tryPeekTQueue . msgQueue
|
||||
tryPeekMsg = tryPeekTBQueue . msgQueue
|
||||
|
||||
peekMsg :: MsgQueue -> STM Message
|
||||
peekMsg = peekTQueue . msgQueue
|
||||
peekMsg = peekTBQueue . msgQueue
|
||||
|
||||
-- atomic delete (== read) last and peek next message if available
|
||||
tryDelPeekMsg :: MsgQueue -> STM (Maybe Message)
|
||||
tryDelPeekMsg (MsgQueue q) = tryReadTQueue q >> tryPeekTQueue q
|
||||
tryDelPeekMsg (MsgQueue q) = tryReadTBQueue q >> tryPeekTBQueue q
|
||||
|
||||
@@ -15,7 +15,7 @@ data QueueRec = QueueRec
|
||||
status :: QueueStatus
|
||||
}
|
||||
|
||||
data QueueStatus = QueueActive | QueueOff
|
||||
data QueueStatus = QueueActive | QueueOff deriving (Eq)
|
||||
|
||||
class MonadQueueStore s m where
|
||||
addQueue :: s -> RecipientPublicKey -> (RecipientId, SenderId) -> m (Either ErrorType ())
|
||||
|
||||
@@ -32,7 +32,7 @@ instance MonadQueueStore QueueStore STM where
|
||||
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,146 @@
|
||||
{-# 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,
|
||||
storeLogFilePath,
|
||||
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
|
||||
|
||||
storeLogFilePath :: StoreLog a -> FilePath
|
||||
storeLogFilePath = \case
|
||||
WriteStoreLog f _ -> f
|
||||
ReadStoreLog f _ -> f
|
||||
|
||||
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
|
||||
@@ -1,42 +1,148 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Transport where
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Transport
|
||||
-- Copyright : (c) simplex.chat
|
||||
-- License : AGPL-3
|
||||
--
|
||||
-- Maintainer : chat@simplex.chat
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
--
|
||||
-- This module defines basic TCP server and client and SMP protocol encrypted transport over TCP.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
|
||||
module Simplex.Messaging.Transport
|
||||
( -- * Transport connection class
|
||||
Transport (..),
|
||||
TProxy (..),
|
||||
ATransport (..),
|
||||
|
||||
import Control.Monad.IO.Class
|
||||
-- * Transport over TCP
|
||||
runTransportServer,
|
||||
runTransportClient,
|
||||
|
||||
-- * TCP transport
|
||||
TCP (..),
|
||||
|
||||
-- * SMP encrypted transport
|
||||
THandle (..),
|
||||
TransportError (..),
|
||||
serverHandshake,
|
||||
clientHandshake,
|
||||
tPutEncrypted,
|
||||
tGetEncrypted,
|
||||
serializeTransportError,
|
||||
transportErrorP,
|
||||
currentSMPVersionStr,
|
||||
|
||||
-- * Trim trailing CR
|
||||
trimCR,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
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.Maybe (fromMaybe)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.String
|
||||
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, parseString)
|
||||
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
|
||||
|
||||
runTCPServer :: MonadUnliftIO m => ServiceName -> (Handle -> m ()) -> m ()
|
||||
runTCPServer port server = do
|
||||
-- * Transport connection class
|
||||
|
||||
class Transport c where
|
||||
transport :: ATransport
|
||||
transport = ATransport (TProxy @c)
|
||||
|
||||
transportName :: TProxy c -> String
|
||||
|
||||
-- | Upgrade client socket to connection (used in the server)
|
||||
getServerConnection :: Socket -> IO c
|
||||
|
||||
-- | Upgrade server socket to connection (used in the client)
|
||||
getClientConnection :: Socket -> IO c
|
||||
|
||||
-- | Close connection
|
||||
closeConnection :: c -> IO ()
|
||||
|
||||
-- | Read fixed number of bytes from connection
|
||||
cGet :: c -> Int -> IO ByteString
|
||||
|
||||
-- | Write bytes to connection
|
||||
cPut :: c -> ByteString -> IO ()
|
||||
|
||||
-- | Receive ByteString from connection, allowing LF or CRLF termination.
|
||||
getLn :: c -> IO ByteString
|
||||
|
||||
-- | Send ByteString to connection terminating it with CRLF.
|
||||
putLn :: c -> ByteString -> IO ()
|
||||
putLn c = cPut c . (<> "\r\n")
|
||||
|
||||
data TProxy c = TProxy
|
||||
|
||||
data ATransport = forall c. Transport c => ATransport (TProxy c)
|
||||
|
||||
-- * Transport over TCP
|
||||
|
||||
-- | Run transport server (plain TCP or WebSockets) on passed TCP port and signal when server started and stopped via passed TMVar.
|
||||
--
|
||||
-- All accepted connections are passed to the passed function.
|
||||
runTransportServer :: (Transport c, MonadUnliftIO m) => TMVar Bool -> ServiceName -> (c -> m ()) -> m ()
|
||||
runTransportServer started port server = do
|
||||
clients <- newTVarIO S.empty
|
||||
E.bracket (liftIO $ startTCPServer port) (liftIO . closeServer clients) $ \sock -> forever $ do
|
||||
h <- liftIO $ acceptTCPConn sock
|
||||
tid <- forkFinally (server h) (const $ IO.hClose h)
|
||||
E.bracket (liftIO $ startTCPServer started port) (liftIO . closeServer clients) \sock -> forever $ do
|
||||
c <- liftIO $ acceptConnection sock
|
||||
tid <- forkFinally (server c) (const $ liftIO $ closeConnection c)
|
||||
atomically . modifyTVar clients $ S.insert tid
|
||||
where
|
||||
closeServer :: TVar (Set ThreadId) -> Socket -> IO ()
|
||||
closeServer clients sock = readTVarIO clients >>= mapM_ killThread >> close sock
|
||||
closeServer clients sock = do
|
||||
readTVarIO clients >>= mapM_ killThread
|
||||
close sock
|
||||
void . atomically $ tryPutTMVar started False
|
||||
acceptConnection :: Transport c => Socket -> IO c
|
||||
acceptConnection sock = accept sock >>= getServerConnection . fst
|
||||
|
||||
startTCPServer :: ServiceName -> IO Socket
|
||||
startTCPServer port = withSocketsDo $ resolve >>= open
|
||||
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
|
||||
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
|
||||
where
|
||||
resolve =
|
||||
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
|
||||
@@ -48,19 +154,16 @@ startTCPServer port = withSocketsDo $ resolve >>= open
|
||||
bind sock $ addrAddress addr
|
||||
listen sock 1024
|
||||
return sock
|
||||
setStarted sock = atomically (tryPutTMVar started True) >> pure sock
|
||||
|
||||
acceptTCPConn :: Socket -> IO Handle
|
||||
acceptTCPConn sock = accept sock >>= getSocketHandle . fst
|
||||
-- | Connect to passed TCP host:port and pass handle to the client.
|
||||
runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> (c -> m a) -> m a
|
||||
runTransportClient host port client = do
|
||||
c <- liftIO $ startTCPClient host port
|
||||
client c `E.finally` liftIO (closeConnection c)
|
||||
|
||||
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 >>= foldM tryOpen (Left err) >>= either E.throwIO return -- replace fold with recursion
|
||||
startTCPClient :: forall c. Transport c => HostName -> ServiceName -> IO c
|
||||
startTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
|
||||
where
|
||||
err :: IOException
|
||||
err = mkIOError NoSuchThing "no address" Nothing Nothing
|
||||
@@ -70,15 +173,29 @@ startTCPClient host port =
|
||||
let hints = defaultHints {addrSocketType = Stream}
|
||||
in getAddrInfo (Just hints) (Just host) (Just port)
|
||||
|
||||
tryOpen :: Exception e => Either e Handle -> AddrInfo -> IO (Either e Handle)
|
||||
tryOpen (Left _) addr = E.try $ open addr
|
||||
tryOpen h _ = return h
|
||||
tryOpen :: IOException -> [AddrInfo] -> IO c
|
||||
tryOpen e [] = E.throwIO e
|
||||
tryOpen _ (addr : as) =
|
||||
E.try (open addr) >>= either (`tryOpen` as) pure
|
||||
|
||||
open :: AddrInfo -> IO Handle
|
||||
open :: AddrInfo -> IO c
|
||||
open addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
connect sock $ addrAddress addr
|
||||
getSocketHandle sock
|
||||
getClientConnection sock
|
||||
|
||||
-- * TCP transport
|
||||
|
||||
newtype TCP = TCP {tcpHandle :: Handle}
|
||||
|
||||
instance Transport TCP where
|
||||
transportName _ = "TCP"
|
||||
getServerConnection = fmap TCP . getSocketHandle
|
||||
getClientConnection = getServerConnection
|
||||
closeConnection = hClose . tcpHandle
|
||||
cGet = B.hGet . tcpHandle
|
||||
cPut = B.hPut . tcpHandle
|
||||
getLn = fmap trimCR . B.hGetLine . tcpHandle
|
||||
|
||||
getSocketHandle :: Socket -> IO Handle
|
||||
getSocketHandle conn = do
|
||||
@@ -88,11 +205,294 @@ getSocketHandle conn = do
|
||||
hSetBuffering h LineBuffering
|
||||
return h
|
||||
|
||||
putLn :: Handle -> ByteString -> IO ()
|
||||
putLn h = B.hPut h . (<> "\r\n")
|
||||
-- | Trim trailing CR from ByteString.
|
||||
trimCR :: ByteString -> ByteString
|
||||
trimCR "" = ""
|
||||
trimCR s = if B.last s == '\r' then B.init s else s
|
||||
|
||||
getLn :: Handle -> IO ByteString
|
||||
getLn h = trim_cr <$> B.hGetLine h
|
||||
-- * SMP encrypted transport
|
||||
|
||||
data SMPVersion = SMPVersion Int Int Int Int
|
||||
deriving (Eq, Ord)
|
||||
|
||||
instance IsString SMPVersion where
|
||||
fromString = parseString $ parseAll smpVersionP
|
||||
|
||||
major :: SMPVersion -> (Int, Int)
|
||||
major (SMPVersion a b _ _) = (a, b)
|
||||
|
||||
currentSMPVersion :: SMPVersion
|
||||
currentSMPVersion = "0.5.1.0"
|
||||
|
||||
currentSMPVersionStr :: ByteString
|
||||
currentSMPVersionStr = serializeSMPVersion currentSMPVersion
|
||||
|
||||
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
|
||||
|
||||
-- | The handle for SMP encrypted transport connection over Transport .
|
||||
data THandle c = THandle
|
||||
{ connection :: c,
|
||||
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
|
||||
}
|
||||
|
||||
-- | Error of SMP encrypted transport over TCP.
|
||||
data TransportError
|
||||
= -- | error parsing transport block
|
||||
TEBadBlock
|
||||
| -- | block encryption error
|
||||
TEEncrypt
|
||||
| -- | block decryption error
|
||||
TEDecrypt
|
||||
| -- | transport handshake error
|
||||
TEHandshake HandshakeError
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
-- | Transport handshake error.
|
||||
data HandshakeError
|
||||
= -- | encryption error
|
||||
ENCRYPT
|
||||
| -- | decryption error
|
||||
DECRYPT
|
||||
| -- | error parsing protocol version
|
||||
VERSION
|
||||
| -- | error parsing RSA key
|
||||
RSA_KEY
|
||||
| -- | error parsing server transport header or invalid block size
|
||||
HEADER
|
||||
| -- | error parsing AES keys
|
||||
AES_KEYS
|
||||
| -- | not matching RSA key hash
|
||||
BAD_HASH
|
||||
| -- | lower major agent version than protocol version
|
||||
MAJOR_VERSION
|
||||
| -- | TCP transport terminated
|
||||
TERMINATED
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance Arbitrary TransportError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
|
||||
|
||||
-- | SMP encrypted transport error parser.
|
||||
transportErrorP :: Parser TransportError
|
||||
transportErrorP =
|
||||
"BLOCK" $> TEBadBlock
|
||||
<|> "AES_ENCRYPT" $> TEEncrypt
|
||||
<|> "AES_DECRYPT" $> TEDecrypt
|
||||
<|> TEHandshake <$> parseRead1
|
||||
|
||||
-- | Serialize SMP encrypted transport error.
|
||||
serializeTransportError :: TransportError -> ByteString
|
||||
serializeTransportError = \case
|
||||
TEEncrypt -> "AES_ENCRYPT"
|
||||
TEDecrypt -> "AES_DECRYPT"
|
||||
TEBadBlock -> "BLOCK"
|
||||
TEHandshake e -> bshow e
|
||||
|
||||
-- | Encrypt and send block to SMP encrypted transport.
|
||||
tPutEncrypted :: Transport c => THandle c -> ByteString -> IO (Either TransportError ())
|
||||
tPutEncrypted THandle {connection = c, sndKey, blockSize} block =
|
||||
encryptBlock sndKey (blockSize - C.authTagSize) block >>= \case
|
||||
Left _ -> pure $ Left TEEncrypt
|
||||
Right (authTag, msg) -> Right <$> cPut c (C.authTagToBS authTag <> msg)
|
||||
|
||||
-- | Receive and decrypt block from SMP encrypted transport.
|
||||
tGetEncrypted :: Transport c => THandle c -> IO (Either TransportError ByteString)
|
||||
tGetEncrypted THandle {connection = c, rcvKey, blockSize} =
|
||||
cGet c 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
|
||||
trim_cr "" = ""
|
||||
trim_cr s = if B.last s == '\r' then B.init s else s
|
||||
(start, rest) = B.splitAt 4 $ C.unIV baseIV
|
||||
iv c = C.IV $ (start `xor` encodeWord32 c) <> rest
|
||||
|
||||
-- | Server SMP encrypted transport handshake.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
|
||||
--
|
||||
-- The numbers in function names refer to the steps in the document.
|
||||
serverHandshake :: forall c. Transport c => c -> Int -> C.FullKeyPair -> ExceptT TransportError IO (THandle c)
|
||||
serverHandshake c srvBlockSize (k, pk) = do
|
||||
checkValidBlockSize srvBlockSize
|
||||
liftIO sendHeaderAndPublicKey_1
|
||||
encryptedKeys <- receiveEncryptedKeys_4
|
||||
ClientHandshake {blockSize, sndKey, rcvKey} <- decryptParseKeys_5 encryptedKeys
|
||||
checkValidBlockSize blockSize
|
||||
th <- liftIO $ transportHandle c rcvKey sndKey blockSize -- keys are swapped here
|
||||
sendWelcome_6 th
|
||||
pure th
|
||||
where
|
||||
sendHeaderAndPublicKey_1 :: IO ()
|
||||
sendHeaderAndPublicKey_1 = do
|
||||
let sKey = C.encodePubKey k
|
||||
header = ServerHeader {blockSize = srvBlockSize, keySize = B.length sKey}
|
||||
cPut c $ binaryServerHeader header
|
||||
cPut c sKey
|
||||
receiveEncryptedKeys_4 :: ExceptT TransportError IO ByteString
|
||||
receiveEncryptedKeys_4 =
|
||||
liftIO (cGet c $ 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 c -> ExceptT TransportError IO ()
|
||||
sendWelcome_6 th = ExceptT . tPutEncrypted th $ currentSMPVersionStr <> " "
|
||||
|
||||
-- | Client SMP encrypted transport handshake.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
|
||||
--
|
||||
-- The numbers in function names refer to the steps in the document.
|
||||
clientHandshake :: forall c. Transport c => c -> Maybe Int -> Maybe C.KeyHash -> ExceptT TransportError IO (THandle c)
|
||||
clientHandshake c blkSize_ keyHash = do
|
||||
mapM_ checkValidBlockSize blkSize_
|
||||
(k, blkSize) <- getHeaderAndPublicKey_1_2
|
||||
let clientBlkSize = fromMaybe blkSize blkSize_
|
||||
chs@ClientHandshake {sndKey, rcvKey} <- liftIO $ generateKeys_3 clientBlkSize
|
||||
sendEncryptedKeys_4 k chs
|
||||
th <- liftIO $ transportHandle c sndKey rcvKey clientBlkSize
|
||||
getWelcome_6 th >>= checkVersion
|
||||
pure th
|
||||
where
|
||||
getHeaderAndPublicKey_1_2 :: ExceptT TransportError IO (C.PublicKey, Int)
|
||||
getHeaderAndPublicKey_1_2 = do
|
||||
header <- liftIO (cGet c serverHeaderSize)
|
||||
ServerHeader {blockSize, keySize} <- liftEither $ parse serverHeaderP (TEHandshake HEADER) header
|
||||
checkValidBlockSize blockSize
|
||||
s <- liftIO $ cGet c 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 chs =
|
||||
liftError (const $ TEHandshake ENCRYPT) (C.encryptOAEP k $ serializeClientHandshake chs)
|
||||
>>= liftIO . cPut c
|
||||
getWelcome_6 :: THandle c -> 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
|
||||
|
||||
checkValidBlockSize :: Int -> ExceptT TransportError IO ()
|
||||
checkValidBlockSize blkSize =
|
||||
when (blkSize `notElem` transportBlockSizes) . throwError $ TEHandshake HEADER
|
||||
|
||||
data ServerHeader = ServerHeader {blockSize :: Int, keySize :: Int}
|
||||
deriving (Eq, Show)
|
||||
|
||||
binaryRsaTransport :: Int
|
||||
binaryRsaTransport = 0
|
||||
|
||||
transportBlockSizes :: [Int]
|
||||
transportBlockSizes = map (* 1024) [4, 8, 16, 32, 64]
|
||||
|
||||
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 :: c -> SessionKey -> SessionKey -> Int -> IO (THandle c)
|
||||
transportHandle c sk rk blockSize = do
|
||||
sndCounter <- newTVarIO 0
|
||||
rcvCounter <- newTVarIO 0
|
||||
pure
|
||||
THandle
|
||||
{ connection = c,
|
||||
sndKey = sk {counter = sndCounter},
|
||||
rcvKey = rk {counter = rcvCounter},
|
||||
blockSize
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
|
||||
module Simplex.Messaging.Transport.WebSockets (WS (..)) where
|
||||
|
||||
import qualified Control.Exception as E
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Network.Socket (Socket)
|
||||
import Network.WebSockets
|
||||
import Network.WebSockets.Stream (Stream)
|
||||
import qualified Network.WebSockets.Stream as S
|
||||
import Simplex.Messaging.Transport (TProxy, Transport (..), TransportError (..), trimCR)
|
||||
|
||||
data WS = WS {wsStream :: Stream, wsConnection :: Connection}
|
||||
|
||||
websocketsOpts :: ConnectionOptions
|
||||
websocketsOpts =
|
||||
defaultConnectionOptions
|
||||
{ connectionCompressionOptions = NoCompression,
|
||||
connectionFramePayloadSizeLimit = SizeLimit 8192,
|
||||
connectionMessageDataSizeLimit = SizeLimit 65536
|
||||
}
|
||||
|
||||
instance Transport WS where
|
||||
transportName :: TProxy WS -> String
|
||||
transportName _ = "WebSockets"
|
||||
|
||||
getServerConnection :: Socket -> IO WS
|
||||
getServerConnection sock = do
|
||||
s <- S.makeSocketStream sock
|
||||
WS s <$> acceptClientRequest s
|
||||
where
|
||||
acceptClientRequest :: Stream -> IO Connection
|
||||
acceptClientRequest s = makePendingConnectionFromStream s websocketsOpts >>= acceptRequest
|
||||
|
||||
getClientConnection :: Socket -> IO WS
|
||||
getClientConnection sock = do
|
||||
s <- S.makeSocketStream sock
|
||||
WS s <$> sendClientRequest s
|
||||
where
|
||||
sendClientRequest :: Stream -> IO Connection
|
||||
sendClientRequest s = newClientConnection s "" "/" websocketsOpts []
|
||||
|
||||
closeConnection :: WS -> IO ()
|
||||
closeConnection = S.close . wsStream
|
||||
|
||||
cGet :: WS -> Int -> IO ByteString
|
||||
cGet c n = do
|
||||
s <- receiveData (wsConnection c)
|
||||
if B.length s == n
|
||||
then pure s
|
||||
else E.throwIO TEBadBlock
|
||||
|
||||
cPut :: WS -> ByteString -> IO ()
|
||||
cPut = sendBinaryData . wsConnection
|
||||
|
||||
getLn :: WS -> IO ByteString
|
||||
getLn c = do
|
||||
s <- trimCR <$> receiveData (wsConnection c)
|
||||
if B.null s || B.last s /= '\n'
|
||||
then E.throwIO TEBadBlock
|
||||
else pure $ B.init s
|
||||
@@ -31,19 +31,33 @@ raceAny_ = r []
|
||||
r as (m : ms) = withAsync m $ \a -> r (a : as) ms
|
||||
r as [] = void $ waitAnyCancel as
|
||||
|
||||
infixl 4 <$$>
|
||||
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 :: (MonadUnliftIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither a = liftIO a >>= liftEither
|
||||
|
||||
liftError :: (MonadUnliftIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
|
||||
liftError :: (MonadIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
|
||||
liftError f = liftEitherError f . runExceptT
|
||||
|
||||
liftEitherError :: (MonadUnliftIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
|
||||
liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
|
||||
liftEitherError f a = liftIOEither (first f <$> a)
|
||||
|
||||
tryError :: MonadError e m => m a -> m (Either e a)
|
||||
tryError action = (Right <$> action) `catchError` (pure . Left)
|
||||
|
||||
ifM :: Monad m => m Bool -> m a -> m a -> m a
|
||||
ifM ba t f = ba >>= \b -> if b then t else f
|
||||
{-# INLINE ifM #-}
|
||||
|
||||
unlessM :: Monad m => m Bool -> m () -> m ()
|
||||
unlessM b = ifM b $ pure ()
|
||||
{-# INLINE unlessM #-}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#
|
||||
# resolver: ./custom-snapshot.yaml
|
||||
# resolver: https://example.com/snapshots/2018-01-01.yaml
|
||||
resolver: lts-16.17
|
||||
resolver: lts-18.21
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
@@ -35,10 +35,8 @@ packages:
|
||||
# forks / in-progress versions pinned to a git hash. For example:
|
||||
#
|
||||
extra-deps:
|
||||
- sqlite-simple-0.4.18.0@sha256:3ceea56375c0a3590c814e411a4eb86943f8d31b93b110ca159c90689b6b39e5,3002
|
||||
- direct-sqlite-2.3.26@sha256:04e835402f1508abca383182023e4e2b9b86297b8533afbd4e57d1a5652e0c23,3718
|
||||
- cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881
|
||||
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
|
||||
- terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
|
||||
# - network-run-0.2.4@sha256:7dbb06def522dab413bce4a46af476820bffdff2071974736b06f52f4ab57c96,885
|
||||
# - git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
|
||||
@@ -2,50 +2,81 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE PostfixOperators #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module AgentTests where
|
||||
module AgentTests (agentTests) where
|
||||
|
||||
import AgentTests.ConnectionRequestTests
|
||||
import AgentTests.FunctionalAPITests (functionalAPITests)
|
||||
import AgentTests.SQLiteTests (storeTests)
|
||||
import Control.Concurrent
|
||||
import Control.Monad (forM_)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Network.HTTP.Types (urlEncode)
|
||||
import SMPAgentClient
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import SMPClient (testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Agent.Protocol as A
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
|
||||
import System.IO (Handle)
|
||||
import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..))
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import System.Directory (removeFile)
|
||||
import System.Timeout
|
||||
import Test.Hspec
|
||||
|
||||
agentTests :: Spec
|
||||
agentTests = do
|
||||
agentTests :: ATransport -> Spec
|
||||
agentTests (ATransport t) = do
|
||||
describe "Connection request" connectionRequestTests
|
||||
describe "Functional API" $ functionalAPITests (ATransport t)
|
||||
describe "SQLite store" storeTests
|
||||
describe "SMP agent protocol syntax" syntaxTests
|
||||
describe "SMP agent protocol syntax" $ syntaxTests t
|
||||
describe "Establishing duplex connection" do
|
||||
it "should connect via one server and one agent" $
|
||||
smpAgentTest2_1 testDuplexConnection
|
||||
smpAgentTest2_1_1 $ testDuplexConnection t
|
||||
it "should connect via one server and one agent (random IDs)" $
|
||||
smpAgentTest2_1_1 $ testDuplexConnRandomIds t
|
||||
it "should connect via one server and 2 agents" $
|
||||
smpAgentTest2 testDuplexConnection
|
||||
smpAgentTest2_2_1 $ testDuplexConnection t
|
||||
it "should connect via one server and 2 agents (random IDs)" $
|
||||
smpAgentTest2_2_1 $ testDuplexConnRandomIds t
|
||||
it "should connect via 2 servers and 2 agents" $
|
||||
smpAgentTest2_2_2 $ testDuplexConnection t
|
||||
it "should connect via 2 servers and 2 agents (random IDs)" $
|
||||
smpAgentTest2_2_2 $ testDuplexConnRandomIds t
|
||||
describe "Establishing connections via `contact connection`" do
|
||||
it "should connect via contact connection with one server and 3 agents" $
|
||||
smpAgentTest3 $ testContactConnection t
|
||||
it "should connect via contact connection with one server and 2 agents (random IDs)" $
|
||||
smpAgentTest2_2_1 $ testContactConnRandomIds t
|
||||
it "should support rejecting contact request" $
|
||||
smpAgentTest2_2_1 $ testRejectContactRequest t
|
||||
describe "Connection subscriptions" do
|
||||
-- TODO replace delays with a permanent fix, this often fails in github build
|
||||
xit "should connect via one server and one agent" $
|
||||
smpAgentTest3_1 testSubscription
|
||||
it "should connect via one server and one agent" $
|
||||
smpAgentTest3_1_1 $ testSubscription t
|
||||
it "should send notifications to client when server disconnects" $
|
||||
smpAgentServerTest testSubscrNotification
|
||||
smpAgentServerTest $ testSubscrNotification t
|
||||
describe "Message delivery" do
|
||||
it "should deliver messages after losing server connection and re-connecting" $
|
||||
smpAgentTest2_2_2_needs_server $ testMsgDeliveryServerRestart t
|
||||
it "should deliver pending messages after agent restarting" $
|
||||
smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t
|
||||
it "should concurrently deliver messages to connections without blocking" $
|
||||
smpAgentTest2_2_1 $ testConcurrentMsgDelivery t
|
||||
it "should deliver messages if one of connections has quota exceeded" $
|
||||
smpAgentTest2_2_1 $ testMsgDeliveryQuotaExceeded t
|
||||
|
||||
-- | 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))
|
||||
-- | receive message to handle `h`
|
||||
(<#:) :: Transport c => c -> IO (ATransmissionOrError 'Agent)
|
||||
(<#:) = tGet SAgent
|
||||
|
||||
-- | send transmission `t` to handle `h` and get response
|
||||
(#:) :: Handle -> (ByteString, ByteString, ByteString) -> IO (ATransmissionOrError 'Agent)
|
||||
h #: t = tPutRaw h t >> tGet SAgent h
|
||||
(#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (ATransmissionOrError 'Agent)
|
||||
h #: t = tPutRaw h t >> (<#:) h
|
||||
|
||||
-- | action and expected response
|
||||
-- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r`
|
||||
@@ -63,15 +94,15 @@ correctTransmission (corrId, cAlias, cmdOrErr) = case cmdOrErr of
|
||||
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)
|
||||
(<#) :: Transport c => c -> ATransmission 'Agent -> Expectation
|
||||
h <# (corrId, cAlias, cmd) = (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)
|
||||
(<#=) :: Transport c => c -> (ATransmission 'Agent -> Bool) -> Expectation
|
||||
h <#= p = (h <#:) >>= (`shouldSatisfy` p . correctTransmission)
|
||||
|
||||
-- | test that nothing is delivered to handle `h` during 10ms
|
||||
(#:#) :: Handle -> String -> Expectation
|
||||
(#:#) :: Transport c => c -> String -> Expectation
|
||||
h #:# err = tryGet `shouldReturn` ()
|
||||
where
|
||||
tryGet =
|
||||
@@ -80,79 +111,328 @@ h #:# err = tryGet `shouldReturn` ()
|
||||
_ -> return ()
|
||||
|
||||
pattern Msg :: MsgBody -> ACommand 'Agent
|
||||
pattern Msg m_body <- MSG {m_body}
|
||||
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
|
||||
|
||||
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)
|
||||
testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testDuplexConnection _ alice bob = do
|
||||
("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW INV")
|
||||
let cReq' = serializeConnReq cReq
|
||||
bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK)
|
||||
("", "bob", Right (CONF confId "bob's connInfo")) <- (alice <#:)
|
||||
alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
|
||||
bob <# ("", "alice", INFO "alice's connInfo")
|
||||
bob <# ("", "alice", CON)
|
||||
alice <# ("", "bob", CON)
|
||||
alice #: ("2", "bob", "SEND :hello") =#> \case ("2", "bob", SENT _) -> True; _ -> False
|
||||
alice #: ("3", "bob", "SEND :how are you?") =#> \case ("3", "bob", SENT _) -> True; _ -> False
|
||||
alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", MID 1)
|
||||
alice <# ("", "bob", SENT 1)
|
||||
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
|
||||
bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK)
|
||||
alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 2)
|
||||
alice <# ("", "bob", SENT 2)
|
||||
bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False
|
||||
bob #: ("14", "alice", "SEND 9\nhello too") =#> \case ("14", "alice", SENT _) -> True; _ -> False
|
||||
bob #: ("13", "alice", "ACK 2") #> ("13", "alice", OK)
|
||||
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 3)
|
||||
bob <# ("", "alice", SENT 3)
|
||||
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
|
||||
bob #: ("15", "alice", "SEND 9\nmessage 1") =#> \case ("15", "alice", SENT _) -> True; _ -> False
|
||||
alice #: ("3a", "bob", "ACK 3") #> ("3a", "bob", OK)
|
||||
bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 4)
|
||||
bob <# ("", "alice", SENT 4)
|
||||
alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False
|
||||
alice #: ("4a", "bob", "ACK 4") #> ("4a", "bob", OK)
|
||||
alice #: ("5", "bob", "OFF") #> ("5", "bob", OK)
|
||||
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", ERR (SMP AUTH))
|
||||
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 5)
|
||||
bob <# ("", "alice", MERR 5 (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)
|
||||
testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testDuplexConnRandomIds _ alice bob = do
|
||||
("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW INV")
|
||||
let cReq' = serializeConnReq cReq
|
||||
("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo")
|
||||
("", bobConn', Right (CONF confId "bob's connInfo")) <- (alice <#:)
|
||||
bobConn' `shouldBe` bobConn
|
||||
alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False
|
||||
bob <# ("", aliceConn, INFO "alice's connInfo")
|
||||
bob <# ("", aliceConn, CON)
|
||||
alice <# ("", bobConn, CON)
|
||||
alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 1)
|
||||
alice <# ("", bobConn, SENT 1)
|
||||
bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False
|
||||
bob #: ("12", aliceConn, "ACK 1") #> ("12", aliceConn, OK)
|
||||
alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 2)
|
||||
alice <# ("", bobConn, SENT 2)
|
||||
bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False
|
||||
bob #: ("13", aliceConn, "ACK 2") #> ("13", aliceConn, OK)
|
||||
bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 3)
|
||||
bob <# ("", aliceConn, SENT 3)
|
||||
alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False
|
||||
alice #: ("3a", bobConn, "ACK 3") #> ("3a", bobConn, OK)
|
||||
bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 4)
|
||||
bob <# ("", aliceConn, SENT 4)
|
||||
alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False
|
||||
alice #: ("4a", bobConn, "ACK 4") #> ("4a", bobConn, OK)
|
||||
alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK)
|
||||
bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 5)
|
||||
bob <# ("", aliceConn, MERR 5 (SMP AUTH))
|
||||
alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK)
|
||||
alice #:# "nothing else should be delivered to alice"
|
||||
|
||||
testContactConnection :: Transport c => TProxy c -> c -> c -> c -> IO ()
|
||||
testContactConnection _ alice bob tom = do
|
||||
("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW CON")
|
||||
let cReq' = serializeConnReq cReq
|
||||
|
||||
bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK)
|
||||
("", "alice_contact", Right (REQ aInvId "bob's connInfo")) <- (alice <#:)
|
||||
alice #: ("2", "bob", "ACPT " <> aInvId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
|
||||
("", "alice", Right (CONF bConfId "alice's connInfo")) <- (bob <#:)
|
||||
bob #: ("12", "alice", "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK)
|
||||
alice <# ("", "bob", INFO "bob's connInfo 2")
|
||||
alice <# ("", "bob", CON)
|
||||
bob <# ("", "alice", CON)
|
||||
alice #: ("3", "bob", "SEND :hi") #> ("3", "bob", MID 1)
|
||||
alice <# ("", "bob", SENT 1)
|
||||
bob <#= \case ("", "alice", Msg "hi") -> True; _ -> False
|
||||
bob #: ("13", "alice", "ACK 1") #> ("13", "alice", OK)
|
||||
|
||||
tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK)
|
||||
("", "alice_contact", Right (REQ aInvId' "tom's connInfo")) <- (alice <#:)
|
||||
alice #: ("4", "tom", "ACPT " <> aInvId' <> " 16\nalice's connInfo") #> ("4", "tom", OK)
|
||||
("", "alice", Right (CONF tConfId "alice's connInfo")) <- (tom <#:)
|
||||
tom #: ("22", "alice", "LET " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK)
|
||||
alice <# ("", "tom", INFO "tom's connInfo 2")
|
||||
alice <# ("", "tom", CON)
|
||||
tom <# ("", "alice", CON)
|
||||
alice #: ("5", "tom", "SEND :hi there") #> ("5", "tom", MID 1)
|
||||
alice <# ("", "tom", SENT 1)
|
||||
tom <#= \case ("", "alice", Msg "hi there") -> True; _ -> False
|
||||
tom #: ("23", "alice", "ACK 1") #> ("23", "alice", OK)
|
||||
|
||||
testContactConnRandomIds :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testContactConnRandomIds _ alice bob = do
|
||||
("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW CON")
|
||||
let cReq' = serializeConnReq cReq
|
||||
|
||||
("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo")
|
||||
("", aliceContact', Right (REQ aInvId "bob's connInfo")) <- (alice <#:)
|
||||
aliceContact' `shouldBe` aliceContact
|
||||
|
||||
("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aInvId <> " 16\nalice's connInfo")
|
||||
("", aliceConn', Right (CONF bConfId "alice's connInfo")) <- (bob <#:)
|
||||
aliceConn' `shouldBe` aliceConn
|
||||
|
||||
bob #: ("12", aliceConn, "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK)
|
||||
alice <# ("", bobConn, INFO "bob's connInfo 2")
|
||||
alice <# ("", bobConn, CON)
|
||||
bob <# ("", aliceConn, CON)
|
||||
|
||||
alice #: ("3", bobConn, "SEND :hi") #> ("3", bobConn, MID 1)
|
||||
alice <# ("", bobConn, SENT 1)
|
||||
bob <#= \case ("", c, Msg "hi") -> c == aliceConn; _ -> False
|
||||
bob #: ("13", aliceConn, "ACK 1") #> ("13", aliceConn, OK)
|
||||
|
||||
testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testRejectContactRequest _ alice bob = do
|
||||
("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW CON")
|
||||
let cReq' = serializeConnReq cReq
|
||||
bob #: ("11", "alice", "JOIN " <> cReq' <> " 10\nbob's info") #> ("11", "alice", OK)
|
||||
("", "a_contact", Right (REQ aInvId "bob's info")) <- (alice <#:)
|
||||
-- RJCT must use correct contact connection
|
||||
alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND)
|
||||
alice #: ("2b", "a_contact", "RJCT " <> aInvId) #> ("2b", "a_contact", OK)
|
||||
alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") #> ("3", "bob", ERR $ A.CMD PROHIBITED)
|
||||
bob #:# "nothing should be delivered to bob"
|
||||
|
||||
testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO ()
|
||||
testSubscription _ alice1 alice2 bob = do
|
||||
(alice1, "alice") `connect` (bob, "bob")
|
||||
bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 1)
|
||||
bob <# ("", "alice", SENT 1)
|
||||
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
|
||||
alice1 #: ("1", "bob", "ACK 1") #> ("1", "bob", OK)
|
||||
bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 2)
|
||||
bob <# ("", "alice", SENT 2)
|
||||
alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False
|
||||
alice1 #: ("2", "bob", "ACK 2") #> ("2", "bob", OK)
|
||||
alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK)
|
||||
alice1 <# ("", "bob", END)
|
||||
bob #: ("14", "alice", "SEND 2\nhi") =#> \case ("14", "alice", SENT _) -> True; _ -> False
|
||||
bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 3)
|
||||
bob <# ("", "alice", SENT 3)
|
||||
alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False
|
||||
alice2 #: ("22", "bob", "ACK 3") #> ("22", "bob", OK)
|
||||
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
|
||||
testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO ()
|
||||
testSubscrNotification t (server, _) client = do
|
||||
client #: ("1", "conn1", "NEW INV") =#> \case ("1", "conn1", INV {}) -> True; _ -> False
|
||||
client #:# "nothing should be delivered to client before the server is killed"
|
||||
killThread server
|
||||
client <# ("", "conn1", END)
|
||||
client <# ("", "conn1", DOWN)
|
||||
withSmpServer (ATransport t) $
|
||||
client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue
|
||||
|
||||
testMsgDeliveryServerRestart :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testMsgDeliveryServerRestart t alice bob = do
|
||||
withServer $ do
|
||||
connect (alice, "alice") (bob, "bob")
|
||||
bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 1)
|
||||
bob <# ("", "alice", SENT 1)
|
||||
alice <#= \case ("", "bob", Msg "hi") -> True; _ -> False
|
||||
alice #: ("11", "bob", "ACK 1") #> ("11", "bob", OK)
|
||||
alice #:# "nothing else delivered before the server is killed"
|
||||
|
||||
alice <# ("", "bob", DOWN)
|
||||
bob #: ("2", "alice", "SEND 11\nhello again") #> ("2", "alice", MID 2)
|
||||
bob #:# "nothing else delivered before the server is restarted"
|
||||
alice #:# "nothing else delivered before the server is restarted"
|
||||
|
||||
withServer $ do
|
||||
bob <# ("", "alice", SENT 2)
|
||||
alice <# ("", "bob", UP)
|
||||
alice <#= \case ("", "bob", Msg "hello again") -> True; _ -> False
|
||||
alice #: ("12", "bob", "ACK 2") #> ("12", "bob", OK)
|
||||
|
||||
removeFile testStoreLogFile
|
||||
where
|
||||
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
|
||||
|
||||
testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO ()
|
||||
testMsgDeliveryAgentRestart t bob = do
|
||||
withAgent $ \alice -> do
|
||||
withServer $ do
|
||||
connect (bob, "bob") (alice, "alice")
|
||||
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 1)
|
||||
alice <# ("", "bob", SENT 1)
|
||||
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
|
||||
bob #: ("11", "alice", "ACK 1") #> ("11", "alice", OK)
|
||||
bob #:# "nothing else delivered before the server is down"
|
||||
|
||||
bob <# ("", "alice", DOWN)
|
||||
alice #: ("2", "bob", "SEND 11\nhello again") #> ("2", "bob", MID 2)
|
||||
alice #:# "nothing else delivered before the server is restarted"
|
||||
bob #:# "nothing else delivered before the server is restarted"
|
||||
|
||||
withAgent $ \alice -> do
|
||||
withServer $ do
|
||||
tPutRaw alice ("3", "bob", "SUB")
|
||||
alice <#= \case
|
||||
(corrId, "bob", cmd) ->
|
||||
(corrId == "3" && cmd == OK)
|
||||
|| (corrId == "" && cmd == SENT 2)
|
||||
_ -> False
|
||||
bob <# ("", "alice", UP)
|
||||
bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False
|
||||
bob #: ("12", "alice", "ACK 2") #> ("12", "alice", OK)
|
||||
|
||||
removeFile testStoreLogFile
|
||||
removeFile testDB
|
||||
where
|
||||
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
|
||||
withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) (pure ()) . const . testSMPAgentClientOn agentTestPort
|
||||
|
||||
testConcurrentMsgDelivery :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testConcurrentMsgDelivery _ alice bob = do
|
||||
connect (alice, "alice") (bob, "bob")
|
||||
|
||||
("1", "bob2", Right (INV cReq)) <- alice #: ("1", "bob2", "NEW INV")
|
||||
let cReq' = serializeConnReq cReq
|
||||
bob #: ("11", "alice2", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice2", OK)
|
||||
("", "bob2", Right (CONF _confId "bob's connInfo")) <- (alice <#:)
|
||||
-- below commands would be needed to accept bob's connection, but alice does not
|
||||
-- alice #: ("2", "bob", "LET " <> _confId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
|
||||
-- bob <# ("", "alice", INFO "alice's connInfo")
|
||||
-- bob <# ("", "alice", CON)
|
||||
-- alice <# ("", "bob", CON)
|
||||
|
||||
-- the first connection should not be blocked by the second one
|
||||
sendMessage (alice, "alice") (bob, "bob") "hello"
|
||||
-- alice #: ("2", "bob", "SEND :hello") #> ("2", "bob", MID 1)
|
||||
-- alice <# ("", "bob", SENT 1)
|
||||
-- bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
|
||||
-- bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK)
|
||||
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 2)
|
||||
bob <# ("", "alice", SENT 2)
|
||||
-- if delivery is blocked it won't go further
|
||||
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
|
||||
alice #: ("3", "bob", "ACK 2") #> ("3", "bob", OK)
|
||||
|
||||
testMsgDeliveryQuotaExceeded :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testMsgDeliveryQuotaExceeded _ alice bob = do
|
||||
connect (alice, "alice") (bob, "bob")
|
||||
connect (alice, "alice2") (bob, "bob2")
|
||||
forM_ [1 .. 4 :: Int] $ \i -> do
|
||||
let corrId = bshow i
|
||||
msg = "message " <> bshow i
|
||||
(_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND :" <> msg)
|
||||
alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False
|
||||
(_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND :over quota")
|
||||
|
||||
alice #: ("1", "bob2", "SEND :hello") #> ("1", "bob2", MID 1)
|
||||
-- if delivery is blocked it won't go further
|
||||
alice <# ("", "bob2", SENT 1)
|
||||
|
||||
connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO ()
|
||||
connect (h1, name1) (h2, name2) = do
|
||||
("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW INV")
|
||||
let cReq' = serializeConnReq cReq
|
||||
h2 #: ("c2", name1, "JOIN " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK)
|
||||
("", _, Right (CONF connId "info2")) <- (h1 <#:)
|
||||
h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK)
|
||||
h2 <# ("", name1, INFO "info1")
|
||||
h2 <# ("", name1, CON)
|
||||
h1 <# ("", name2, CON)
|
||||
|
||||
sendMessage :: Transport c => (c, ConnId) -> (c, ConnId) -> ByteString -> IO ()
|
||||
sendMessage (h1, name1) (h2, name2) msg = do
|
||||
("m1", name2', Right (MID mId)) <- h1 #: ("m1", name2, "SEND :" <> msg)
|
||||
name2' `shouldBe` name2
|
||||
h1 <#= \case ("", n, SENT m) -> n == name2 && m == mId; _ -> False
|
||||
("", name1', Right (MSG MsgMeta {recipient = (msgId, _)} msg')) <- (h2 <#:)
|
||||
name1' `shouldBe` name1
|
||||
msg' `shouldBe` msg
|
||||
h2 #: ("m2", name1, "ACK " <> bshow msgId) =#> \case ("m2", n, OK) -> n == name1; _ -> False
|
||||
|
||||
-- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString)
|
||||
-- connect' h1 h2 = do
|
||||
-- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW INV")
|
||||
-- let cReq' = serializeConnReq cReq
|
||||
-- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> cReq' <> " 5\ninfo2")
|
||||
-- ("", _, Right (REQ connId "info2")) <- (h1 <#:)
|
||||
-- h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False
|
||||
-- h2 <# ("", conn1, INFO "info1")
|
||||
-- h2 <# ("", conn1, CON)
|
||||
-- h1 <# ("", conn2, CON)
|
||||
-- pure (conn1, conn2)
|
||||
|
||||
samplePublicKey :: ByteString
|
||||
samplePublicKey = "256,ppr3DCweAD3RTVFhU2j0u+DnYdqJl1qCdKLHIKsPl1xBzfmnzK0o9GEDlaIClbK39KzPJMljcpnYb2KlSoZ51AhwF5PH2CS+FStc3QzajiqfdOQPet23Hd9YC6pqyTQ7idntqgPrE7yKJF44lUhKlq8QS9KQcbK7W6t7F9uQFw44ceWd2eVf81UV04kQdKWJvC5Sz6jtSZNEfs9mVI8H0wi1amUvS6+7EDJbxikhcCRnFShFO9dUKRYXj6L2JVqXqO5cZgY9BScyneWIg6mhhsTcdDbITM6COlL+pF1f3TjDN+slyV+IzE+ap/9NkpsrCcI8KwwDpqEDmUUV/JQfmQ==,gj2UAiWzSj7iun0iXvI5iz5WEjaqngmB3SzQ5+iarixbaG15LFDtYs3pijG3eGfB1wIFgoP4D2z97vIWn8olT4uCTUClf29zGDDve07h/B3QG/4i0IDnio7MX3AbE8O6PKouqy/GLTfT4WxFUn423g80rpsVYd5oj+SCL2eaxIc="
|
||||
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 SYNTAX 11")
|
||||
syntaxTests :: forall c. Transport c => TProxy c -> Spec
|
||||
syntaxTests t = 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#1234") >#>= \case ("213", "", "INV" : _) -> True; _ -> False
|
||||
it "with port and keyHash" $ ("214", "", "NEW localhost:5000#1234") >#>= \case ("214", "", "INV" : _) -> True; _ -> False
|
||||
it "with correct parameter" $ ("211", "", "NEW INV") >#>= \case ("211", _, "INV" : _) -> True; _ -> False
|
||||
describe "invalid" do
|
||||
-- TODO: add tests with defined connection alias
|
||||
it "no parameters" $ ("221", "", "NEW") >#> ("221", "", "ERR SYNTAX 11")
|
||||
it "many parameters" $ ("222", "", "NEW localhost:5000 hi") >#> ("222", "", "ERR SYNTAX 11")
|
||||
it "invalid server keyHash" $ ("223", "", "NEW localhost:5000#1") >#> ("223", "", "ERR SYNTAX 11")
|
||||
it "with incorrect parameter" $ ("222", "", "NEW hi") >#> ("222", "", "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")
|
||||
("311", "a", "JOIN https://simpex.chat/invitation#/?smp=smp%3A%2F%2Flocalhost%3A5000%2F1234-w%3D%3D%23&e2e=" <> urlEncode True samplePublicKey <> " 14\nbob's connInfo") >#> ("311", "a", "ERR SMP AUTH")
|
||||
describe "invalid" do
|
||||
-- TODO: JOIN is not merged yet - to be added
|
||||
it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR SYNTAX 11")
|
||||
it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX")
|
||||
where
|
||||
-- simple test for one command with the expected response
|
||||
(>#>) :: ARawTransmission -> ARawTransmission -> Expectation
|
||||
command >#> response = smpAgentTest t command `shouldReturn` response
|
||||
|
||||
-- simple test for one command with a predicate for the expected response
|
||||
(>#>=) :: ARawTransmission -> ((ByteString, ByteString, [ByteString]) -> Bool) -> Expectation
|
||||
command >#>= p = smpAgentTest t command >>= (`shouldSatisfy` p . \(cId, cAlias, cmd) -> (cId, cAlias, B.words cmd))
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module AgentTests.ConnectionRequestTests where
|
||||
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Test.Hspec
|
||||
|
||||
uri :: String
|
||||
uri = "smp.simplex.im"
|
||||
|
||||
srv :: SMPServer
|
||||
srv =
|
||||
SMPServer
|
||||
{ host = "smp.simplex.im",
|
||||
port = Just "5223",
|
||||
keyHash = Just (C.KeyHash "\215m\248\251")
|
||||
}
|
||||
|
||||
queue :: SMPQueueUri
|
||||
queue =
|
||||
SMPQueueUri
|
||||
{ smpServer = srv,
|
||||
senderId = "\215m\248\251",
|
||||
serverVerifyKey = reservedServerKey
|
||||
}
|
||||
|
||||
appServer :: ConnReqScheme
|
||||
appServer = CRSAppServer "simplex.chat" Nothing
|
||||
|
||||
connectionRequest :: AConnectionRequest
|
||||
connectionRequest =
|
||||
ACR SCMInvitation . CRInvitation $
|
||||
ConnReqData
|
||||
{ crScheme = appServer,
|
||||
crSmpQueues = [queue],
|
||||
crEncryptKey = reservedServerKey
|
||||
}
|
||||
|
||||
connectionRequestTests :: Spec
|
||||
connectionRequestTests = do
|
||||
describe "connection request parsing / serializing" $ do
|
||||
it "should serialize SMP queue URIs" $ do
|
||||
serializeSMPQueueUri queue {smpServer = srv {port = Nothing, keyHash = Nothing}}
|
||||
`shouldBe` "smp://smp.simplex.im/1234-w==#"
|
||||
serializeSMPQueueUri queue {smpServer = srv {keyHash = Nothing}}
|
||||
`shouldBe` "smp://smp.simplex.im:5223/1234-w==#"
|
||||
serializeSMPQueueUri queue {smpServer = srv {port = Nothing}}
|
||||
`shouldBe` "smp://1234-w==@smp.simplex.im/1234-w==#"
|
||||
serializeSMPQueueUri queue
|
||||
`shouldBe` "smp://1234-w==@smp.simplex.im:5223/1234-w==#"
|
||||
it "should parse SMP queue URIs" $ do
|
||||
parseAll smpQueueUriP "smp://smp.simplex.im/1234-w==#"
|
||||
`shouldBe` Right queue {smpServer = srv {port = Nothing, keyHash = Nothing}}
|
||||
parseAll smpQueueUriP "smp://smp.simplex.im:5223/1234-w==#"
|
||||
`shouldBe` Right queue {smpServer = srv {keyHash = Nothing}}
|
||||
parseAll smpQueueUriP "smp://1234-w==@smp.simplex.im/1234-w==#"
|
||||
`shouldBe` Right queue {smpServer = srv {port = Nothing}}
|
||||
parseAll smpQueueUriP "smp://1234-w==@smp.simplex.im:5223/1234-w==#"
|
||||
`shouldBe` Right queue
|
||||
it "should serialize connection requests" $ do
|
||||
serializeConnReq connectionRequest
|
||||
`shouldBe` "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D"
|
||||
it "should parse connection requests" $ do
|
||||
parseAll connReqP "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D"
|
||||
`shouldBe` Right connectionRequest
|
||||
@@ -0,0 +1,163 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module AgentTests.FunctionalAPITests (functionalAPITests) where
|
||||
|
||||
import Control.Monad.Except (ExceptT, runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import SMPAgentClient
|
||||
import SMPClient (withSmpServer)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (dbFile)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
|
||||
import Simplex.Messaging.Transport (ATransport (..))
|
||||
import System.Timeout
|
||||
import Test.Hspec
|
||||
import UnliftIO.STM
|
||||
|
||||
(##>) :: MonadIO m => m (ATransmission 'Agent) -> ATransmission 'Agent -> m ()
|
||||
a ##> t = a >>= \t' -> liftIO (t' `shouldBe` t)
|
||||
|
||||
(=##>) :: MonadIO m => m (ATransmission 'Agent) -> (ATransmission 'Agent -> Bool) -> m ()
|
||||
a =##> p = a >>= \t -> liftIO (t `shouldSatisfy` p)
|
||||
|
||||
get :: MonadIO m => AgentClient -> m (ATransmission 'Agent)
|
||||
get c = atomically (readTBQueue $ subQ c)
|
||||
|
||||
pattern Msg :: MsgBody -> ACommand 'Agent
|
||||
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
|
||||
|
||||
functionalAPITests :: ATransport -> Spec
|
||||
functionalAPITests t = do
|
||||
describe "Establishing duplex connection" $
|
||||
it "should connect via one server using SMP agent clients" $
|
||||
withSmpServer t testAgentClient
|
||||
describe "Establishing connection asynchronously" $ do
|
||||
it "should connect with initiating client going offline" $
|
||||
withSmpServer t testAsyncInitiatingOffline
|
||||
it "should connect with joining client going offline before its queue activation" $
|
||||
withSmpServer t testAsyncJoiningOfflineBeforeActivation
|
||||
-- TODO a valid test case but not trivial to implement, probably requires some agent rework
|
||||
xit "should connect with joining client going offline after its queue activation" $
|
||||
withSmpServer t testAsyncJoiningOfflineAfterActivation
|
||||
it "should connect with both clients going offline" $
|
||||
withSmpServer t testAsyncBothOffline
|
||||
|
||||
testAgentClient :: IO ()
|
||||
testAgentClient = do
|
||||
alice <- getSMPAgentClient cfg
|
||||
bob <- getSMPAgentClient cfg {dbFile = testDB2}
|
||||
Right () <- runExceptT $ do
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
("", _, CONF confId "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get bob ##> ("", aliceId, CON)
|
||||
1 <- sendMessage alice bobId "hello"
|
||||
get alice ##> ("", bobId, SENT 1)
|
||||
2 <- sendMessage alice bobId "how are you?"
|
||||
get alice ##> ("", bobId, SENT 2)
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 1
|
||||
get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 2
|
||||
3 <- sendMessage bob aliceId "hello too"
|
||||
get bob ##> ("", aliceId, SENT 3)
|
||||
4 <- sendMessage bob aliceId "message 1"
|
||||
get bob ##> ("", aliceId, SENT 4)
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 3
|
||||
get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 4
|
||||
suspendConnection alice bobId
|
||||
5 <- sendMessage bob aliceId "message 2"
|
||||
get bob ##> ("", aliceId, MERR 5 (SMP AUTH))
|
||||
deleteConnection alice bobId
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
pure ()
|
||||
where
|
||||
noMessages :: AgentClient -> String -> Expectation
|
||||
noMessages c err = tryGet `shouldReturn` ()
|
||||
where
|
||||
tryGet =
|
||||
10000 `timeout` get c >>= \case
|
||||
Just _ -> error err
|
||||
_ -> return ()
|
||||
|
||||
testAsyncInitiatingOffline :: IO ()
|
||||
testAsyncInitiatingOffline = do
|
||||
alice <- getSMPAgentClient cfg
|
||||
bob <- getSMPAgentClient cfg {dbFile = testDB2}
|
||||
Right () <- runExceptT $ do
|
||||
(bobId, cReq) <- createConnection alice SCMInvitation
|
||||
disconnectAgentClient alice
|
||||
aliceId <- joinConnection bob cReq "bob's connInfo"
|
||||
alice' <- liftIO $ getSMPAgentClient cfg
|
||||
subscribeConnection alice' bobId
|
||||
("", _, CONF confId "bob's connInfo") <- get alice'
|
||||
allowConnection alice' bobId confId "alice's connInfo"
|
||||
get alice' ##> ("", bobId, CON)
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get bob ##> ("", aliceId, CON)
|
||||
exchangeGreetings alice' bobId bob aliceId
|
||||
pure ()
|
||||
|
||||
testAsyncJoiningOfflineBeforeActivation :: IO ()
|
||||
testAsyncJoiningOfflineBeforeActivation = do
|
||||
alice <- getSMPAgentClient cfg
|
||||
bob <- getSMPAgentClient cfg {dbFile = testDB2}
|
||||
Right () <- runExceptT $ do
|
||||
(bobId, qInfo) <- createConnection alice SCMInvitation
|
||||
aliceId <- joinConnection bob qInfo "bob's connInfo"
|
||||
disconnectAgentClient bob
|
||||
("", _, CONF confId "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2}
|
||||
subscribeConnection bob' aliceId
|
||||
get alice ##> ("", bobId, CON)
|
||||
get bob' ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get bob' ##> ("", aliceId, CON)
|
||||
exchangeGreetings alice bobId bob' aliceId
|
||||
pure ()
|
||||
|
||||
testAsyncJoiningOfflineAfterActivation :: IO ()
|
||||
testAsyncJoiningOfflineAfterActivation = error "not implemented"
|
||||
|
||||
testAsyncBothOffline :: IO ()
|
||||
testAsyncBothOffline = do
|
||||
alice <- getSMPAgentClient cfg
|
||||
bob <- getSMPAgentClient cfg {dbFile = testDB2}
|
||||
Right () <- runExceptT $ do
|
||||
(bobId, cReq) <- createConnection alice SCMInvitation
|
||||
disconnectAgentClient alice
|
||||
aliceId <- joinConnection bob cReq "bob's connInfo"
|
||||
disconnectAgentClient bob
|
||||
alice' <- liftIO $ getSMPAgentClient cfg
|
||||
subscribeConnection alice' bobId
|
||||
("", _, CONF confId "bob's connInfo") <- get alice'
|
||||
allowConnection alice' bobId confId "alice's connInfo"
|
||||
bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2}
|
||||
subscribeConnection bob' aliceId
|
||||
get alice' ##> ("", bobId, CON)
|
||||
get bob' ##> ("", aliceId, INFO "alice's connInfo")
|
||||
get bob' ##> ("", aliceId, CON)
|
||||
exchangeGreetings alice' bobId bob' aliceId
|
||||
pure ()
|
||||
|
||||
exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
|
||||
exchangeGreetings alice bobId bob aliceId = do
|
||||
1 <- sendMessage alice bobId "hello"
|
||||
get alice ##> ("", bobId, SENT 1)
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 1
|
||||
2 <- sendMessage bob aliceId "hello too"
|
||||
get bob ##> ("", aliceId, SENT 2)
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 2
|
||||
@@ -1,43 +1,65 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module AgentTests.SQLiteTests (storeTests) where
|
||||
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (replicateM_)
|
||||
import Control.Monad.Except (ExceptT, runExceptT)
|
||||
import qualified Crypto.PubKey.RSA as R
|
||||
import Crypto.Random (drgNew)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time
|
||||
import Data.Word (Word32)
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import SMPClient (testKeyHash)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import System.Random (Random (randomIO))
|
||||
import System.Random
|
||||
import Test.Hspec
|
||||
import UnliftIO.Directory (removeFile)
|
||||
|
||||
testDB :: String
|
||||
testDB = "smp-agent.test.db"
|
||||
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
|
||||
withStore2 :: SpecWith (SQLiteStore, SQLiteStore) -> Spec
|
||||
withStore2 = before connect2 . after (removeStore . fst)
|
||||
where
|
||||
connect2 :: IO (SQLiteStore, SQLiteStore)
|
||||
connect2 = do
|
||||
s1 <- createStore
|
||||
s2 <- connectSQLiteStore (dbFilePath s1) 4
|
||||
pure (s1, s2)
|
||||
|
||||
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) 4 Migrations.app
|
||||
|
||||
removeStore :: SQLiteStore -> IO ()
|
||||
removeStore store = do
|
||||
close store
|
||||
removeFile $ dbFilePath store
|
||||
where
|
||||
close :: SQLiteStore -> IO ()
|
||||
close st = mapM_ DB.close =<< atomically (flushTBQueue $ dbConnPool st)
|
||||
|
||||
returnsResult :: (Eq a, Eq e, Show a, Show e) => ExceptT e IO a -> a -> Expectation
|
||||
action `returnsResult` r = runExceptT action `shouldReturn` Right r
|
||||
@@ -47,42 +69,73 @@ action `throwsError` e = runExceptT action `shouldReturn` Left e
|
||||
|
||||
-- TODO add null port tests
|
||||
storeTests :: Spec
|
||||
storeTests = withStore do
|
||||
describe "compiled as threadsafe" testCompiledThreadsafe
|
||||
describe "foreign keys enabled" testForeignKeysEnabled
|
||||
describe "store methods" do
|
||||
describe "createRcvConn" testCreateRcvConn
|
||||
describe "createSndConn" testCreateSndConn
|
||||
describe "getAllConnAliases" testGetAllConnAliases
|
||||
describe "getRcvQueue" testGetRcvQueue
|
||||
describe "deleteConn" do
|
||||
describe "RcvConnection" testDeleteRcvConn
|
||||
describe "SndConnection" testDeleteSndConn
|
||||
describe "DuplexConnection" testDeleteDuplexConn
|
||||
describe "upgradeRcvConnToDuplex" testUpgradeRcvConnToDuplex
|
||||
describe "upgradeSndConnToDuplex" testUpgradeSndConnToDuplex
|
||||
describe "set queue status" do
|
||||
describe "setRcvQueueStatus" testSetRcvQueueStatus
|
||||
describe "setSndQueueStatus" testSetSndQueueStatus
|
||||
describe "DuplexConnection" testSetQueueStatusDuplex
|
||||
xdescribe "RcvQueue doesn't exist" testSetRcvQueueStatusNoQueue
|
||||
xdescribe "SndQueue doesn't exist" testSetSndQueueStatusNoQueue
|
||||
describe "createRcvMsg" do
|
||||
describe "RcvQueue exists" testCreateRcvMsg
|
||||
describe "RcvQueue doesn't exist" testCreateRcvMsgNoQueue
|
||||
describe "createSndMsg" do
|
||||
describe "SndQueue exists" testCreateSndMsg
|
||||
describe "SndQueue doesn't exist" testCreateSndMsgNoQueue
|
||||
storeTests = do
|
||||
withStore2 do
|
||||
describe "stress test" testConcurrentWrites
|
||||
withStore do
|
||||
describe "store setup" do
|
||||
testCompiledThreadsafe
|
||||
testForeignKeysEnabled
|
||||
describe "store methods" do
|
||||
describe "Queue and Connection management" do
|
||||
describe "createRcvConn" do
|
||||
testCreateRcvConn
|
||||
testCreateRcvConnRandomId
|
||||
testCreateRcvConnDuplicate
|
||||
describe "createSndConn" do
|
||||
testCreateSndConn
|
||||
testCreateSndConnRandomID
|
||||
testCreateSndConnDuplicate
|
||||
describe "getAllConnIds" testGetAllConnIds
|
||||
describe "getRcvConn" testGetRcvConn
|
||||
describe "deleteConn" do
|
||||
testDeleteRcvConn
|
||||
testDeleteSndConn
|
||||
testDeleteDuplexConn
|
||||
describe "upgradeRcvConnToDuplex" do
|
||||
testUpgradeRcvConnToDuplex
|
||||
describe "upgradeSndConnToDuplex" do
|
||||
testUpgradeSndConnToDuplex
|
||||
describe "set Queue status" do
|
||||
describe "setRcvQueueStatus" do
|
||||
testSetRcvQueueStatus
|
||||
testSetRcvQueueStatusNoQueue
|
||||
describe "setSndQueueStatus" do
|
||||
testSetSndQueueStatus
|
||||
testSetSndQueueStatusNoQueue
|
||||
testSetQueueStatusDuplex
|
||||
describe "Msg management" do
|
||||
describe "create Msg" do
|
||||
testCreateRcvMsg
|
||||
testCreateSndMsg
|
||||
testCreateRcvAndSndMsgs
|
||||
|
||||
testConcurrentWrites :: SpecWith (SQLiteStore, SQLiteStore)
|
||||
testConcurrentWrites =
|
||||
it "should complete multiple concurrent write transactions w/t sqlite busy errors" $ \(s1, s2) -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn s1 g cData1 rcvQueue1 SCMInvitation
|
||||
let ConnData {connId} = cData1
|
||||
concurrently_ (runTest s1 connId) (runTest s2 connId)
|
||||
where
|
||||
runTest :: SQLiteStore -> ConnId -> IO (Either StoreError ())
|
||||
runTest store connId = runExceptT . replicateM_ 100 $ do
|
||||
(internalId, internalRcvId, _, _) <- updateRcvIds store connId
|
||||
let rcvMsgData = mkRcvMsgData internalId internalRcvId 0 "0" "hash_dummy"
|
||||
createRcvMsg store connId rcvMsgData
|
||||
|
||||
testCompiledThreadsafe :: SpecWith SQLiteStore
|
||||
testCompiledThreadsafe = do
|
||||
it "should throw error if compiled sqlite library is not threadsafe" $ \store -> do
|
||||
compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]]
|
||||
testCompiledThreadsafe =
|
||||
it "compiled sqlite library should be threadsafe" . withStoreConnection $ \db -> do
|
||||
compileOptions <- DB.query_ db "pragma COMPILE_OPTIONS;" :: IO [[T.Text]]
|
||||
compileOptions `shouldNotContain` [["THREADSAFE=0"]]
|
||||
|
||||
withStoreConnection :: (DB.Connection -> IO a) -> SQLiteStore -> IO a
|
||||
withStoreConnection = flip withConnection
|
||||
|
||||
testForeignKeysEnabled :: SpecWith SQLiteStore
|
||||
testForeignKeysEnabled = do
|
||||
it "should throw error if foreign keys are enabled" $ \store -> do
|
||||
testForeignKeysEnabled =
|
||||
it "foreign keys should be enabled" . withStoreConnection $ \db -> do
|
||||
let inconsistentQuery =
|
||||
[sql|
|
||||
INSERT INTO connections
|
||||
@@ -90,19 +143,20 @@ testForeignKeysEnabled = do
|
||||
VALUES
|
||||
("conn1", "smp.simplex.im", "5223", "1234", "smp.simplex.im", "5223", "2345");
|
||||
|]
|
||||
DB.execute_ (dbConn store) inconsistentQuery
|
||||
DB.execute_ db inconsistentQuery
|
||||
`shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint)
|
||||
|
||||
cData1 :: ConnData
|
||||
cData1 = ConnData {connId = "conn1"}
|
||||
|
||||
rcvQueue1 :: RcvQueue
|
||||
rcvQueue1 =
|
||||
RcvQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") (Just "1234"),
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
rcvId = "1234",
|
||||
connAlias = "conn1",
|
||||
rcvPrivateKey = C.PrivateKey 1 2 3,
|
||||
rcvPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
sndId = Just "2345",
|
||||
sndKey = Nothing,
|
||||
decryptKey = C.PrivateKey 1 2 3,
|
||||
decryptKey = C.safePrivateKey (1, 2, 3),
|
||||
verifyKey = Nothing,
|
||||
status = New
|
||||
}
|
||||
@@ -110,241 +164,311 @@ rcvQueue1 =
|
||||
sndQueue1 :: SndQueue
|
||||
sndQueue1 =
|
||||
SndQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") (Just "1234"),
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
sndId = "3456",
|
||||
connAlias = "conn1",
|
||||
sndPrivateKey = C.PrivateKey 1 2 3,
|
||||
sndPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
encryptKey = C.PublicKey $ R.PublicKey 1 2 3,
|
||||
signKey = C.PrivateKey 1 2 3,
|
||||
signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey),
|
||||
status = New
|
||||
}
|
||||
|
||||
testCreateRcvConn :: SpecWith SQLiteStore
|
||||
testCreateRcvConn = do
|
||||
testCreateRcvConn =
|
||||
it "should create RcvConnection and add SndQueue" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
`returnsResult` "conn1"
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
|
||||
testCreateRcvConnRandomId :: SpecWith SQLiteStore
|
||||
testCreateRcvConnRandomId =
|
||||
it "should create RcvConnection and add SndQueue with random ID" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
Right connId <- runExceptT $ createRcvConn store g cData1 {connId = ""} rcvQueue1 SCMInvitation
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 {connId} rcvQueue1)
|
||||
upgradeRcvConnToDuplex store connId sndQueue1
|
||||
`returnsResult` ()
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1)
|
||||
|
||||
testCreateRcvConnDuplicate :: SpecWith SQLiteStore
|
||||
testCreateRcvConnDuplicate =
|
||||
it "should throw error on attempt to create duplicate RcvConnection" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
`throwsError` SEConnDuplicate
|
||||
|
||||
testCreateSndConn :: SpecWith SQLiteStore
|
||||
testCreateSndConn = do
|
||||
testCreateSndConn =
|
||||
it "should create SndConnection and add RcvQueue" $ \store -> do
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
createSndConn store g cData1 sndQueue1
|
||||
`returnsResult` "conn1"
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
|
||||
upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
|
||||
testGetAllConnAliases :: SpecWith SQLiteStore
|
||||
testGetAllConnAliases = do
|
||||
testCreateSndConnRandomID :: SpecWith SQLiteStore
|
||||
testCreateSndConnRandomID =
|
||||
it "should create SndConnection and add RcvQueue with random ID" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
Right connId <- runExceptT $ createSndConn store g cData1 {connId = ""} sndQueue1
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 {connId} sndQueue1)
|
||||
upgradeSndConnToDuplex store connId rcvQueue1
|
||||
`returnsResult` ()
|
||||
getConn store connId
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1)
|
||||
|
||||
testCreateSndConnDuplicate :: SpecWith SQLiteStore
|
||||
testCreateSndConnDuplicate =
|
||||
it "should throw error on attempt to create duplicate SndConnection" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
createSndConn store g cData1 sndQueue1
|
||||
`throwsError` SEConnDuplicate
|
||||
|
||||
testGetAllConnIds :: SpecWith SQLiteStore
|
||||
testGetAllConnIds =
|
||||
it "should get all conn aliases" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
createSndConn store sndQueue1 {connAlias = "conn2"}
|
||||
`returnsResult` ()
|
||||
getAllConnAliases store
|
||||
`returnsResult` ["conn1" :: ConnAlias, "conn2" :: ConnAlias]
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ createSndConn store g cData1 {connId = "conn2"} sndQueue1
|
||||
getAllConnIds store
|
||||
`returnsResult` ["conn1" :: ConnId, "conn2" :: ConnId]
|
||||
|
||||
testGetRcvQueue :: SpecWith SQLiteStore
|
||||
testGetRcvQueue = do
|
||||
it "should get RcvQueue" $ \store -> do
|
||||
let smpServer = SMPServer "smp.simplex.im" (Just "5223") (Just "1234")
|
||||
testGetRcvConn :: SpecWith SQLiteStore
|
||||
testGetRcvConn =
|
||||
it "should get connection using rcv queue id and server" $ \store -> do
|
||||
let smpServer = SMPServer "smp.simplex.im" (Just "5223") testKeyHash
|
||||
let recipientId = "1234"
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
getRcvQueue store smpServer recipientId
|
||||
`returnsResult` rcvQueue1
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
getRcvConn store smpServer recipientId
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
|
||||
testDeleteRcvConn :: SpecWith SQLiteStore
|
||||
testDeleteRcvConn = do
|
||||
testDeleteRcvConn =
|
||||
it "should create RcvConnection and delete it" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEBadConn
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testDeleteSndConn :: SpecWith SQLiteStore
|
||||
testDeleteSndConn = do
|
||||
testDeleteSndConn =
|
||||
it "should create SndConnection and delete it" $ \store -> do
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEBadConn
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testDeleteDuplexConn :: SpecWith SQLiteStore
|
||||
testDeleteDuplexConn = do
|
||||
testDeleteDuplexConn =
|
||||
it "should create DuplexConnection and delete it" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
deleteConn store "conn1"
|
||||
`returnsResult` ()
|
||||
-- TODO check queues are deleted as well
|
||||
getConn store "conn1"
|
||||
`throwsError` SEBadConn
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testUpgradeRcvConnToDuplex :: SpecWith SQLiteStore
|
||||
testUpgradeRcvConnToDuplex = do
|
||||
testUpgradeRcvConnToDuplex =
|
||||
it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" $ \store -> do
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
let anotherSndQueue =
|
||||
SndQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") (Just "1234"),
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
sndId = "2345",
|
||||
connAlias = "conn1",
|
||||
sndPrivateKey = C.PrivateKey 1 2 3,
|
||||
sndPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
encryptKey = C.PublicKey $ R.PublicKey 1 2 3,
|
||||
signKey = C.PrivateKey 1 2 3,
|
||||
signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey),
|
||||
status = New
|
||||
}
|
||||
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
|
||||
`throwsError` SEBadConnType CSnd
|
||||
upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
`returnsResult` ()
|
||||
_ <- runExceptT $ upgradeSndConnToDuplex store "conn1" rcvQueue1
|
||||
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
|
||||
`throwsError` SEBadConnType CDuplex
|
||||
|
||||
testUpgradeSndConnToDuplex :: SpecWith SQLiteStore
|
||||
testUpgradeSndConnToDuplex = do
|
||||
testUpgradeSndConnToDuplex =
|
||||
it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
let anotherRcvQueue =
|
||||
RcvQueue
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") (Just "1234"),
|
||||
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
|
||||
rcvId = "3456",
|
||||
connAlias = "conn1",
|
||||
rcvPrivateKey = C.PrivateKey 1 2 3,
|
||||
rcvPrivateKey = C.safePrivateKey (1, 2, 3),
|
||||
sndId = Just "4567",
|
||||
sndKey = Nothing,
|
||||
decryptKey = C.PrivateKey 1 2 3,
|
||||
decryptKey = C.safePrivateKey (1, 2, 3),
|
||||
verifyKey = Nothing,
|
||||
status = New
|
||||
}
|
||||
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
|
||||
`throwsError` SEBadConnType CRcv
|
||||
upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
`returnsResult` ()
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
|
||||
`throwsError` SEBadConnType CDuplex
|
||||
|
||||
testSetRcvQueueStatus :: SpecWith SQLiteStore
|
||||
testSetRcvQueueStatus = do
|
||||
testSetRcvQueueStatus =
|
||||
it "should update status of RcvQueue" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
|
||||
setRcvQueueStatus store rcvQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1 {status = Confirmed})
|
||||
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1 {status = Confirmed})
|
||||
|
||||
testSetSndQueueStatus :: SpecWith SQLiteStore
|
||||
testSetSndQueueStatus = do
|
||||
testSetSndQueueStatus =
|
||||
it "should update status of SndQueue" $ \store -> do
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1 {status = Confirmed})
|
||||
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1 {status = Confirmed})
|
||||
|
||||
testSetQueueStatusDuplex :: SpecWith SQLiteStore
|
||||
testSetQueueStatusDuplex = do
|
||||
testSetQueueStatusDuplex =
|
||||
it "should update statuses of RcvQueue and SndQueue in DuplexConnection" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
`returnsResult` ()
|
||||
g <- newTVarIO =<< drgNew
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
|
||||
setRcvQueueStatus store rcvQueue1 Secured
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1)
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1)
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`returnsResult` ()
|
||||
getConn store "conn1"
|
||||
`returnsResult` SomeConn
|
||||
SCDuplex
|
||||
( DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed}
|
||||
)
|
||||
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed})
|
||||
|
||||
testSetRcvQueueStatusNoQueue :: SpecWith SQLiteStore
|
||||
testSetRcvQueueStatusNoQueue = do
|
||||
it "should throw error on attempt to update status of nonexistent RcvQueue" $ \store -> do
|
||||
testSetRcvQueueStatusNoQueue =
|
||||
xit "should throw error on attempt to update status of non-existent RcvQueue" $ \store -> do
|
||||
setRcvQueueStatus store rcvQueue1 Confirmed
|
||||
`throwsError` SEInternal
|
||||
`throwsError` SEConnNotFound
|
||||
|
||||
testSetSndQueueStatusNoQueue :: SpecWith SQLiteStore
|
||||
testSetSndQueueStatusNoQueue = do
|
||||
it "should throw error on attempt to update status of nonexistent SndQueue" $ \store -> do
|
||||
testSetSndQueueStatusNoQueue =
|
||||
xit "should throw error on attempt to update status of non-existent SndQueue" $ \store -> do
|
||||
setSndQueueStatus store sndQueue1 Confirmed
|
||||
`throwsError` SEInternal
|
||||
`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
|
||||
{ internalRcvId,
|
||||
msgMeta =
|
||||
MsgMeta
|
||||
{ integrity = MsgOk,
|
||||
recipient = (unId internalId, ts),
|
||||
sender = (externalSndId, ts),
|
||||
broker = (brokerId, ts)
|
||||
},
|
||||
msgBody = hw,
|
||||
internalHash,
|
||||
externalPrevSndHash = "hash_from_sender"
|
||||
}
|
||||
|
||||
testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation
|
||||
testCreateRcvMsg' st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do
|
||||
let MsgMeta {recipient = (internalId, _)} = msgMeta
|
||||
updateRcvIds st connId
|
||||
`returnsResult` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)
|
||||
createRcvMsg st connId rcvMsgData
|
||||
`returnsResult` ()
|
||||
|
||||
testCreateRcvMsg :: SpecWith SQLiteStore
|
||||
testCreateRcvMsg = do
|
||||
it "should create a RcvMsg and return InternalId" $ \store -> do
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
testCreateRcvMsg =
|
||||
it "should reserve internal ids and create a RcvMsg" $ \st -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
let ConnData {connId} = cData1
|
||||
_ <- runExceptT $ createRcvConn st g cData1 rcvQueue1 SCMInvitation
|
||||
-- TODO getMsg to check message
|
||||
let ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
|
||||
createRcvMsg store "conn1" (encodeUtf8 "Hello world!") ts (1, ts) ("1", ts)
|
||||
`returnsResult` InternalId 1
|
||||
testCreateRcvMsg' st 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy"
|
||||
testCreateRcvMsg' st 1 "hash_dummy" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy"
|
||||
|
||||
testCreateRcvMsgNoQueue :: SpecWith SQLiteStore
|
||||
testCreateRcvMsgNoQueue = do
|
||||
it "should throw error on attempt to create a RcvMsg w/t a RcvQueue" $ \store -> do
|
||||
let ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
|
||||
createRcvMsg store "conn1" (encodeUtf8 "Hello world!") ts (1, ts) ("1", ts)
|
||||
`throwsError` SEBadConn
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
createRcvMsg store "conn1" (encodeUtf8 "Hello world!") ts (1, ts) ("1", ts)
|
||||
`throwsError` SEBadConnType CSnd
|
||||
mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData
|
||||
mkSndMsgData internalId internalSndId internalHash =
|
||||
SndMsgData
|
||||
{ internalId,
|
||||
internalSndId,
|
||||
internalTs = ts,
|
||||
msgBody = hw,
|
||||
internalHash,
|
||||
previousMsgHash = internalHash
|
||||
}
|
||||
|
||||
testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation
|
||||
testCreateSndMsg' store expectedPrevHash connId sndMsgData@SndMsgData {..} = do
|
||||
updateSndIds store connId
|
||||
`returnsResult` (internalId, internalSndId, expectedPrevHash)
|
||||
createSndMsg store connId sndMsgData
|
||||
`returnsResult` ()
|
||||
|
||||
testCreateSndMsg :: SpecWith SQLiteStore
|
||||
testCreateSndMsg = do
|
||||
it "should create a SndMsg and return InternalId" $ \store -> do
|
||||
createSndConn store sndQueue1
|
||||
`returnsResult` ()
|
||||
testCreateSndMsg =
|
||||
it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
let ConnData {connId} = cData1
|
||||
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
|
||||
-- TODO getMsg to check message
|
||||
let ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
|
||||
createSndMsg store "conn1" (encodeUtf8 "Hello world!") ts
|
||||
`returnsResult` InternalId 1
|
||||
testCreateSndMsg' store "" connId $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy"
|
||||
testCreateSndMsg' store "hash_dummy" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy"
|
||||
|
||||
testCreateSndMsgNoQueue :: SpecWith SQLiteStore
|
||||
testCreateSndMsgNoQueue = do
|
||||
it "should throw error on attempt to create a SndMsg w/t a SndQueue" $ \store -> do
|
||||
let ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
|
||||
createSndMsg store "conn1" (encodeUtf8 "Hello world!") ts
|
||||
`throwsError` SEBadConn
|
||||
createRcvConn store rcvQueue1
|
||||
`returnsResult` ()
|
||||
createSndMsg store "conn1" (encodeUtf8 "Hello world!") ts
|
||||
`throwsError` SEBadConnType CRcv
|
||||
testCreateRcvAndSndMsgs :: SpecWith SQLiteStore
|
||||
testCreateRcvAndSndMsgs =
|
||||
it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \store -> do
|
||||
g <- newTVarIO =<< drgNew
|
||||
let ConnData {connId} = cData1
|
||||
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
|
||||
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
|
||||
testCreateRcvMsg' store 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1"
|
||||
testCreateRcvMsg' store 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2"
|
||||
testCreateSndMsg' store "" connId $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1"
|
||||
testCreateRcvMsg' store 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
|
||||
testCreateSndMsg' store "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
|
||||
testCreateSndMsg' store "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
module ProtocolErrorTests where
|
||||
|
||||
import Simplex.Messaging.Agent.Protocol (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)
|
||||
@@ -1,146 +1,197 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module SMPAgentClient where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import SMPClient (testPort, withSmpServer, withSmpServerThreadOn)
|
||||
import Simplex.Messaging.Agent
|
||||
import SMPClient
|
||||
( serverBracket,
|
||||
testKeyHash,
|
||||
testPort,
|
||||
testPort2,
|
||||
withSmpServer,
|
||||
withSmpServerOn,
|
||||
withSmpServerThreadOn,
|
||||
)
|
||||
import Simplex.Messaging.Agent (runSMPAgentBlocking)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client (SMPClientConfig (..), smpDefaultConfig)
|
||||
import Simplex.Messaging.Transport
|
||||
import Test.Hspec
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Directory
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.IO
|
||||
|
||||
agentTestHost :: HostName
|
||||
agentTestHost = "localhost"
|
||||
|
||||
agentTestPort :: ServiceName
|
||||
agentTestPort = "5001"
|
||||
agentTestPort = "5010"
|
||||
|
||||
agentTestPort2 :: ServiceName
|
||||
agentTestPort2 = "5011"
|
||||
|
||||
agentTestPort3 :: ServiceName
|
||||
agentTestPort3 = "5021"
|
||||
agentTestPort3 = "5012"
|
||||
|
||||
testDB :: String
|
||||
testDB = "smp-agent.test.protocol.db"
|
||||
testDB = "tests/tmp/smp-agent.test.protocol.db"
|
||||
|
||||
testDB2 :: String
|
||||
testDB2 = "smp-agent2.test.protocol.db"
|
||||
testDB2 = "tests/tmp/smp-agent2.test.protocol.db"
|
||||
|
||||
testDB3 :: String
|
||||
testDB3 = "smp-agent3.test.protocol.db"
|
||||
testDB3 = "tests/tmp/smp-agent3.test.protocol.db"
|
||||
|
||||
smpAgentTest :: ARawTransmission -> IO ARawTransmission
|
||||
smpAgentTest cmd = runSmpAgentTest $ \h -> tPutRaw h cmd >> tGetRaw h
|
||||
smpAgentTest :: forall c. Transport c => TProxy c -> ARawTransmission -> IO ARawTransmission
|
||||
smpAgentTest _ cmd = runSmpAgentTest $ \(h :: c) -> tPutRaw h cmd >> tGetRaw h
|
||||
|
||||
runSmpAgentTest :: (MonadUnliftIO m, MonadRandom m) => (Handle -> m a) -> m a
|
||||
runSmpAgentTest test = withSmpServer . withSmpAgent $ testSMPAgentClient test
|
||||
runSmpAgentTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => (c -> m a) -> m a
|
||||
runSmpAgentTest test = withSmpServer t . withSmpAgent t $ testSMPAgentClient test
|
||||
where
|
||||
t = transport @c
|
||||
|
||||
runSmpAgentServerTest :: (MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> Handle -> m a) -> m a
|
||||
runSmpAgentServerTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> c -> m a) -> m a
|
||||
runSmpAgentServerTest test =
|
||||
withSmpServerThreadOn testPort $
|
||||
\server -> withSmpAgentThreadOn (agentTestPort, testDB) $
|
||||
withSmpServerThreadOn t testPort $
|
||||
\server -> withSmpAgentThreadOn t (agentTestPort, testPort, testDB) $
|
||||
\agent -> testSMPAgentClient $ test (server, agent)
|
||||
where
|
||||
t = transport @c
|
||||
|
||||
smpAgentServerTest :: ((ThreadId, ThreadId) -> Handle -> IO ()) -> Expectation
|
||||
smpAgentServerTest :: Transport c => ((ThreadId, ThreadId) -> c -> 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 []
|
||||
runSmpAgentTestN :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, String)] -> ([c] -> m a) -> m a
|
||||
runSmpAgentTestN agents test = withSmpServer t $ run agents []
|
||||
where
|
||||
run :: [(ServiceName, String)] -> [Handle] -> m a
|
||||
run :: [(ServiceName, ServiceName, String)] -> [c] -> m a
|
||||
run [] hs = test hs
|
||||
run (a@(p, _) : as) hs = withSmpAgentOn a $ testSMPAgentClientOn p $ \h -> run as (h : hs)
|
||||
run (a@(p, _, _) : as) hs = withSmpAgentOn t a $ testSMPAgentClientOn p $ \h -> run as (h : hs)
|
||||
t = transport @c
|
||||
|
||||
runSmpAgentTestN_1 :: forall m a. (MonadUnliftIO m, MonadRandom m) => Int -> ([Handle] -> m a) -> m a
|
||||
runSmpAgentTestN_1 nClients test = withSmpServer . withSmpAgent $ run nClients []
|
||||
runSmpAgentTestN_1 :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => Int -> ([c] -> m a) -> m a
|
||||
runSmpAgentTestN_1 nClients test = withSmpServer t . withSmpAgent t $ run nClients []
|
||||
where
|
||||
run :: Int -> [Handle] -> m a
|
||||
run :: Int -> [c] -> m a
|
||||
run 0 hs = test hs
|
||||
run n hs = testSMPAgentClient $ \h -> run (n - 1) (h : hs)
|
||||
t = transport @c
|
||||
|
||||
smpAgentTestN :: [(ServiceName, String)] -> ([Handle] -> IO ()) -> Expectation
|
||||
smpAgentTestN :: Transport c => [(ServiceName, ServiceName, String)] -> ([c] -> IO ()) -> Expectation
|
||||
smpAgentTestN agents test' = runSmpAgentTestN agents test' `shouldReturn` ()
|
||||
|
||||
smpAgentTestN_1 :: Int -> ([Handle] -> IO ()) -> Expectation
|
||||
smpAgentTestN_1 :: Transport c => Int -> ([c] -> IO ()) -> Expectation
|
||||
smpAgentTestN_1 n test' = runSmpAgentTestN_1 n test' `shouldReturn` ()
|
||||
|
||||
smpAgentTest2 :: (Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest2 test' =
|
||||
smpAgentTestN [(agentTestPort, testDB), (agentTestPort2, testDB2)] _test
|
||||
smpAgentTest2_2_2 :: forall c. Transport c => (c -> c -> IO ()) -> Expectation
|
||||
smpAgentTest2_2_2 test' =
|
||||
withSmpServerOn (transport @c) testPort2 $
|
||||
smpAgentTest2_2_2_needs_server test'
|
||||
|
||||
smpAgentTest2_2_2_needs_server :: forall c. Transport c => (c -> c -> IO ()) -> Expectation
|
||||
smpAgentTest2_2_2_needs_server test' =
|
||||
smpAgentTestN
|
||||
[ (agentTestPort, testPort, testDB),
|
||||
(agentTestPort2, testPort2, testDB2)
|
||||
]
|
||||
_test
|
||||
where
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpAgentTest2_1 :: (Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest2_1 test' = smpAgentTestN_1 2 _test
|
||||
smpAgentTest2_2_1 :: Transport c => (c -> c -> IO ()) -> Expectation
|
||||
smpAgentTest2_2_1 test' =
|
||||
smpAgentTestN
|
||||
[ (agentTestPort, testPort, testDB),
|
||||
(agentTestPort2, testPort, testDB2)
|
||||
]
|
||||
_test
|
||||
where
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpAgentTest3 :: (Handle -> Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest2_1_1 :: Transport c => (c -> c -> IO ()) -> Expectation
|
||||
smpAgentTest2_1_1 test' = smpAgentTestN_1 2 _test
|
||||
where
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpAgentTest3 :: Transport c => (c -> c -> c -> IO ()) -> Expectation
|
||||
smpAgentTest3 test' =
|
||||
smpAgentTestN
|
||||
[(agentTestPort, testDB), (agentTestPort2, testDB2), (agentTestPort3, testDB3)]
|
||||
[ (agentTestPort, testPort, testDB),
|
||||
(agentTestPort2, testPort, testDB2),
|
||||
(agentTestPort3, testPort, testDB3)
|
||||
]
|
||||
_test
|
||||
where
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
smpAgentTest3_1 :: (Handle -> Handle -> Handle -> IO ()) -> Expectation
|
||||
smpAgentTest3_1 test' = smpAgentTestN_1 3 _test
|
||||
smpAgentTest3_1_1 :: Transport c => (c -> c -> c -> IO ()) -> Expectation
|
||||
smpAgentTest3_1_1 test' = smpAgentTestN_1 3 _test
|
||||
where
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
smpAgentTest1_1_1 :: forall c. Transport c => (c -> IO ()) -> Expectation
|
||||
smpAgentTest1_1_1 test' =
|
||||
smpAgentTestN
|
||||
[(agentTestPort2, testPort2, testDB2)]
|
||||
_test
|
||||
where
|
||||
_test [h] = test' h
|
||||
_test _ = error "expected 1 handle"
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
defaultAgentConfig
|
||||
{ tcpPort = agentTestPort,
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
smpServers = L.fromList ["localhost:5000#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="],
|
||||
tbqSize = 1,
|
||||
dbFile = testDB,
|
||||
smpCfg =
|
||||
smpDefaultConfig
|
||||
{ qSize = 1,
|
||||
defaultPort = testPort,
|
||||
defaultTransport = (testPort, transport @TCP),
|
||||
tcpTimeout = 500_000
|
||||
}
|
||||
},
|
||||
retryInterval = (retryInterval defaultAgentConfig) {initialInterval = 50_000}
|
||||
}
|
||||
|
||||
withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => (ServiceName, String) -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn (port', db') =
|
||||
E.bracket
|
||||
(forkIOWithUnmask ($ runSMPAgent cfg {tcpPort = port', dbFile = db'}))
|
||||
(liftIO . killThread >=> const (removeFile db'))
|
||||
withSmpAgentThreadOn_ :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m () -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn_ t (port', smpPort', db') afterProcess =
|
||||
let cfg' = cfg {tcpPort = port', dbFile = db', smpServers = L.fromList [SMPServer "localhost" (Just smpPort') testKeyHash]}
|
||||
in serverBracket
|
||||
(\started -> runSMPAgentBlocking t started cfg')
|
||||
afterProcess
|
||||
|
||||
withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => (ServiceName, String) -> m a -> m a
|
||||
withSmpAgentOn (port', db') = withSmpAgentThreadOn (port', db') . const
|
||||
withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a $ removeFile db'
|
||||
|
||||
withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => m a -> m a
|
||||
withSmpAgent = withSmpAgentOn (agentTestPort, testDB)
|
||||
withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a
|
||||
withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const
|
||||
|
||||
testSMPAgentClientOn :: MonadUnliftIO m => ServiceName -> (Handle -> m a) -> m a
|
||||
withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a
|
||||
withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB)
|
||||
|
||||
testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m a) -> m a
|
||||
testSMPAgentClientOn port' client = do
|
||||
threadDelay 500_000 -- TODO hack: thread delay for SMP agent to start
|
||||
runTCPClient agentTestHost port' $ \h -> do
|
||||
runTransportClient agentTestHost port' $ \h -> do
|
||||
line <- liftIO $ getLn h
|
||||
if line == "Welcome to SMP v0.2.0 agent"
|
||||
if line == "Welcome to SMP agent v" <> currentSMPVersionStr
|
||||
then client h
|
||||
else error "not connected"
|
||||
else error $ "wrong welcome message: " <> B.unpack line
|
||||
|
||||
testSMPAgentClient :: MonadUnliftIO m => (Handle -> m a) -> m a
|
||||
testSMPAgentClient :: (Transport c, MonadUnliftIO m) => (c -> m a) -> m a
|
||||
testSMPAgentClient = testSMPAgentClientOn agentTestPort
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module SMPClient where
|
||||
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Base64 (encode)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Network.Socket
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server
|
||||
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 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"
|
||||
@@ -23,63 +33,141 @@ testHost = "localhost"
|
||||
testPort :: ServiceName
|
||||
testPort = "5000"
|
||||
|
||||
testSMPClient :: MonadUnliftIO m => (Handle -> m a) -> m a
|
||||
testSMPClient client = do
|
||||
threadDelay 250_000 -- TODO hack: thread delay for SMP server to start
|
||||
runTCPClient testHost testPort $ \h -> do
|
||||
line <- liftIO $ getLn h
|
||||
if line == "Welcome to SMP v0.2.0"
|
||||
then client h
|
||||
else error "not connected"
|
||||
testPort2 :: ServiceName
|
||||
testPort2 = "5001"
|
||||
|
||||
testKeyHashStr :: B.ByteString
|
||||
testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
|
||||
|
||||
testBlockSize :: Maybe Int
|
||||
testBlockSize = Just 8192
|
||||
|
||||
testKeyHash :: Maybe C.KeyHash
|
||||
testKeyHash = Just "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
|
||||
|
||||
testStoreLogFile :: FilePath
|
||||
testStoreLogFile = "tests/tmp/smp-server-store.log"
|
||||
|
||||
testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a
|
||||
testSMPClient client =
|
||||
runTransportClient testHost testPort $ \h ->
|
||||
liftIO (runExceptT $ clientHandshake h testBlockSize testKeyHash) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
|
||||
cfg :: ServerConfig
|
||||
cfg =
|
||||
ServerConfig
|
||||
{ tcpPort = testPort,
|
||||
{ transports = undefined,
|
||||
tbqSize = 1,
|
||||
serverTbqSize = 1,
|
||||
msgQueueQuota = 4,
|
||||
queueIdBytes = 12,
|
||||
msgIdBytes = 6
|
||||
msgIdBytes = 6,
|
||||
storeLog = Nothing,
|
||||
blockSize = 8192,
|
||||
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="
|
||||
}
|
||||
|
||||
withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ServiceName -> (ThreadId -> m a) -> m a
|
||||
withSmpServerThreadOn port =
|
||||
withSmpServerStoreLogOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
|
||||
withSmpServerStoreLogOn t port client = do
|
||||
s <- liftIO $ openReadStoreLog testStoreLogFile
|
||||
serverBracket
|
||||
(\started -> runSMPServerBlocking started cfg {transports = [(port, t)], storeLog = Just s})
|
||||
(pure ())
|
||||
client
|
||||
|
||||
withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
|
||||
withSmpServerThreadOn t port =
|
||||
serverBracket
|
||||
(\started -> runSMPServerBlocking started cfg {transports = [(port, t)]})
|
||||
(pure ())
|
||||
|
||||
serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a
|
||||
serverBracket process afterProcess f = do
|
||||
started <- newEmptyTMVarIO
|
||||
E.bracket
|
||||
(forkIOWithUnmask ($ runSMPServer cfg {tcpPort = port}))
|
||||
(liftIO . killThread)
|
||||
|
||||
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) => (Handle -> 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 []
|
||||
(forkIOWithUnmask ($ process started))
|
||||
(\t -> killThread t >> afterProcess >> waitFor started "stop")
|
||||
(\t -> waitFor started "start" >> f t)
|
||||
where
|
||||
run :: Int -> [Handle] -> m a
|
||||
waitFor started s =
|
||||
5_000_000 `timeout` atomically (takeTMVar started) >>= \case
|
||||
Nothing -> error $ "server did not " <> s
|
||||
_ -> pure ()
|
||||
|
||||
withSmpServerOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> m a -> m a
|
||||
withSmpServerOn t port = withSmpServerThreadOn t port . const
|
||||
|
||||
withSmpServer :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a
|
||||
withSmpServer t = withSmpServerOn t testPort
|
||||
|
||||
runSmpTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => (THandle c -> m a) -> m a
|
||||
runSmpTest test = withSmpServer (transport @c) $ testSMPClient test
|
||||
|
||||
runSmpTestN :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => Int -> ([THandle c] -> m a) -> m a
|
||||
runSmpTestN nClients test = withSmpServer (transport @c) $ run nClients []
|
||||
where
|
||||
run :: Int -> [THandle c] -> m a
|
||||
run 0 hs = test hs
|
||||
run n hs = testSMPClient $ \h -> run (n - 1) (h : hs)
|
||||
|
||||
smpServerTest :: RawTransmission -> IO RawTransmission
|
||||
smpServerTest cmd = runSmpTest $ \h -> tPutRaw h cmd >> tGetRaw h
|
||||
smpServerTest :: forall c. Transport c => TProxy c -> RawTransmission -> IO RawTransmission
|
||||
smpServerTest _ cmd = runSmpTest $ \(h :: THandle c) -> tPutRaw h cmd >> tGetRaw h
|
||||
|
||||
smpTest :: (Handle -> IO ()) -> Expectation
|
||||
smpTest test' = runSmpTest test' `shouldReturn` ()
|
||||
smpTest :: Transport c => TProxy c -> (THandle c -> IO ()) -> Expectation
|
||||
smpTest _ test' = runSmpTest test' `shouldReturn` ()
|
||||
|
||||
smpTestN :: Int -> ([Handle] -> IO ()) -> Expectation
|
||||
smpTestN :: Transport c => Int -> ([THandle c] -> IO ()) -> Expectation
|
||||
smpTestN n test' = runSmpTestN n test' `shouldReturn` ()
|
||||
|
||||
smpTest2 :: (Handle -> Handle -> IO ()) -> Expectation
|
||||
smpTest2 test' = smpTestN 2 _test
|
||||
smpTest2 :: Transport c => TProxy c -> (THandle c -> THandle c -> 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 test' = smpTestN 3 _test
|
||||
smpTest3 :: Transport c => TProxy c -> (THandle c -> THandle c -> THandle c -> IO ()) -> Expectation
|
||||
smpTest3 _ test' = smpTestN 3 _test
|
||||
where
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
tPutRaw :: Transport c => THandle c -> RawTransmission -> IO ()
|
||||
tPutRaw h (sig, corrId, queueId, command) = do
|
||||
let t = B.intercalate " " [corrId, queueId, command]
|
||||
void $ tPut h (C.Signature sig, t)
|
||||
|
||||
tGetRaw :: Transport c => THandle c -> IO RawTransmission
|
||||
tGetRaw h = do
|
||||
("", (CorrId corrId, qId, Right cmd)) <- tGet fromServer h
|
||||
pure ("", corrId, encode qId, serializeCommand cmd)
|
||||
|
||||
@@ -4,64 +4,71 @@
|
||||
{-# 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, 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 System.IO (Handle)
|
||||
import Simplex.Messaging.Transport
|
||||
import System.Directory (removeFile)
|
||||
import System.TimeIt (timeItT)
|
||||
import System.Timeout
|
||||
import Test.HUnit
|
||||
import Test.Hspec
|
||||
|
||||
rsaKeySize :: Int
|
||||
rsaKeySize = 1024 `div` 8
|
||||
rsaKeySize = 2048 `div` 8
|
||||
|
||||
serverTests :: Spec
|
||||
serverTests = do
|
||||
describe "SMP syntax" syntaxTests
|
||||
serverTests :: ATransport -> Spec
|
||||
serverTests t = do
|
||||
describe "SMP syntax" $ syntaxTests t
|
||||
describe "SMP queues" do
|
||||
describe "NEW and KEY commands, SEND messages" testCreateSecure
|
||||
describe "NEW, OFF and DEL commands, SEND messages" testCreateDelete
|
||||
describe "NEW and KEY commands, SEND messages" $ testCreateSecure t
|
||||
describe "NEW, OFF and DEL commands, SEND messages" $ testCreateDelete t
|
||||
describe "Stress test" $ stressTest t
|
||||
describe "SMP messages" do
|
||||
describe "duplex communication over 2 SMP connections" testDuplex
|
||||
describe "switch subscription to another SMP queue" testSwitchSub
|
||||
describe "duplex communication over 2 SMP connections" $ testDuplex t
|
||||
describe "switch subscription to another SMP queue" $ testSwitchSub t
|
||||
describe "Store log" $ testWithStoreLog t
|
||||
describe "Timing of AUTH error" $ testTiming t
|
||||
|
||||
pattern Resp :: CorrId -> QueueId -> Command 'Broker -> SignedTransmissionOrError
|
||||
pattern Resp corrId queueId command <- ("", (corrId, queueId, Right (Cmd SBroker command)))
|
||||
|
||||
sendRecv :: Handle -> (ByteString, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
|
||||
sendRecv :: Transport c => THandle c -> (ByteString, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
|
||||
sendRecv h (sgn, corrId, qId, cmd) = tPutRaw h (sgn, corrId, encode qId, cmd) >> tGet fromServer h
|
||||
|
||||
signSendRecv :: Handle -> C.PrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
|
||||
signSendRecv :: Transport c => THandle c -> C.SafePrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
|
||||
signSendRecv h pk (corrId, qId, cmd) = do
|
||||
let t = B.intercalate "\r\n" [corrId, encode qId, cmd]
|
||||
Right sig <- C.sign pk t
|
||||
tPut h (sig, t)
|
||||
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 =
|
||||
testCreateSecure :: ATransport -> Spec
|
||||
testCreateSecure (ATransport t) =
|
||||
it "should create (NEW) and secure (KEY) queue" $
|
||||
smpTest \h -> do
|
||||
smpTest t $ \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\r\nhello")
|
||||
Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, "SEND 5 hello ")
|
||||
(ok1, OK) #== "accepts unsigned SEND"
|
||||
(sId1, sId) #== "same queue ID in response 1"
|
||||
|
||||
@@ -72,15 +79,15 @@ testCreateSecure =
|
||||
(ok4, OK) #== "replies OK when message acknowledged if no more messages"
|
||||
|
||||
Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, "ACK")
|
||||
(err6, ERR PROHIBITED) #== "replies ERR when message acknowledged without messages"
|
||||
(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\r\nhello")
|
||||
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 ("12345678", "bcda", rId, keyCmd)
|
||||
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)
|
||||
@@ -93,7 +100,7 @@ testCreateSecure =
|
||||
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\r\nhello again")
|
||||
Resp "bcda" _ ok3 <- signSendRecv h sKey ("bcda", sId, "SEND 11 hello again ")
|
||||
(ok3, OK) #== "accepts signed SEND"
|
||||
|
||||
Resp "" _ (MSG _ _ msg) <- tGet fromServer h
|
||||
@@ -102,13 +109,13 @@ testCreateSecure =
|
||||
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\r\nhello")
|
||||
Resp "dabc" _ err5 <- sendRecv h ("", "dabc", sId, "SEND 5 hello ")
|
||||
(err5, ERR AUTH) #== "rejects unsigned SEND"
|
||||
|
||||
testCreateDelete :: Spec
|
||||
testCreateDelete =
|
||||
testCreateDelete :: ATransport -> Spec
|
||||
testCreateDelete (ATransport t) =
|
||||
it "should create (NEW), suspend (OFF) and delete (DEL) queue" $
|
||||
smpTest2 \rh sh -> do
|
||||
smpTest2 t $ \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"
|
||||
@@ -117,16 +124,16 @@ testCreateDelete =
|
||||
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\r\nhello")
|
||||
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\r\nhello 2")
|
||||
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 ("12345678", "abcd", rId, "OFF")
|
||||
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")
|
||||
@@ -136,10 +143,10 @@ testCreateDelete =
|
||||
(ok3, OK) #== "suspends queue"
|
||||
(rId2, rId) #== "same queue ID in response 2"
|
||||
|
||||
Resp "dabc" _ err3 <- signSendRecv sh sKey ("dabc", sId, "SEND 5\r\nhello")
|
||||
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\r\nhello")
|
||||
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")
|
||||
@@ -148,7 +155,7 @@ testCreateDelete =
|
||||
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 ("12345678", "dabc", rId, "DEL")
|
||||
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")
|
||||
@@ -158,10 +165,10 @@ testCreateDelete =
|
||||
(ok6, OK) #== "deletes queue"
|
||||
(rId3, rId) #== "same queue ID in response 3"
|
||||
|
||||
Resp "cdab" _ err7 <- signSendRecv sh sKey ("cdab", sId, "SEND 5\r\nhello")
|
||||
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\r\nhello")
|
||||
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")
|
||||
@@ -173,10 +180,26 @@ testCreateDelete =
|
||||
Resp "cdab" _ err10 <- signSendRecv rh rKey ("cdab", rId, "SUB")
|
||||
(err10, ERR AUTH) #== "rejects SUB when deleted"
|
||||
|
||||
testDuplex :: Spec
|
||||
testDuplex =
|
||||
stressTest :: ATransport -> Spec
|
||||
stressTest (ATransport t) =
|
||||
it "should create many queues, disconnect and re-connect" $
|
||||
smpTest3 t $ \h1 h2 h3 -> do
|
||||
(rPub, rKey) <- C.generateKeyPair rsaKeySize
|
||||
rIds <- forM [1 .. 50 :: Int] . const $ do
|
||||
Resp "" "" (IDS rId _) <- signSendRecv h1 rKey ("", "", "NEW " <> C.serializePubKey rPub)
|
||||
pure rId
|
||||
let subscribeQueues h = forM_ rIds $ \rId -> do
|
||||
Resp "" rId' OK <- signSendRecv h rKey ("", rId, "SUB")
|
||||
rId' `shouldBe` rId
|
||||
closeConnection $ connection h1
|
||||
subscribeQueues h2
|
||||
closeConnection $ connection h2
|
||||
subscribeQueues h3
|
||||
|
||||
testDuplex :: ATransport -> Spec
|
||||
testDuplex (ATransport t) =
|
||||
it "should create 2 simplex connections and exchange messages" $
|
||||
smpTest2 \alice bob -> do
|
||||
smpTest2 t $ \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
|
||||
@@ -211,7 +234,7 @@ testDuplex =
|
||||
(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\r\nhi alice")
|
||||
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")
|
||||
@@ -223,13 +246,13 @@ testDuplex =
|
||||
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, "ACK")
|
||||
(msg5, "how are you bob") #== "message received from alice"
|
||||
|
||||
testSwitchSub :: Spec
|
||||
testSwitchSub =
|
||||
testSwitchSub :: ATransport -> Spec
|
||||
testSwitchSub (ATransport t) =
|
||||
it "should create simplex connections and switch subscription to another TCP connection" $
|
||||
smpTest3 \rh1 rh2 sh -> do
|
||||
smpTest3 t $ \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\r\ntest1")
|
||||
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"
|
||||
@@ -246,13 +269,13 @@ testSwitchSub =
|
||||
Resp "" _ end <- tGet fromServer rh1
|
||||
(end, END) #== "unsubscribed the 1st TCP connection"
|
||||
|
||||
Resp "dabc" _ OK <- sendRecv sh ("", "dabc", sId, "SEND 5\r\ntest3")
|
||||
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 PROHIBITED) #== "rejects ACK from the 1st TCP connection"
|
||||
(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"
|
||||
@@ -261,40 +284,156 @@ testSwitchSub =
|
||||
Nothing -> return ()
|
||||
Just _ -> error "nothing else is delivered to the 1st TCP connection"
|
||||
|
||||
syntaxTests :: Spec
|
||||
syntaxTests = do
|
||||
it "unknown command" $ ("", "abcd", "1234", "HELLO") >#> ("", "abcd", "1234", "ERR SYNTAX 2")
|
||||
testWithStoreLog :: ATransport -> Spec
|
||||
testWithStoreLog at@(ATransport t) =
|
||||
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 at testPort . runTest t $ \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 at testPort . runTest t $ \h -> do
|
||||
sId1 <- readTVarIO senderId1
|
||||
-- fails if store log is disabled
|
||||
Resp "bcda" _ (ERR AUTH) <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
|
||||
pure ()
|
||||
|
||||
withSmpServerStoreLogOn at testPort . runTest t $ \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 :: Transport c => THandle c -> 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 :: Transport c => TProxy c -> (THandle c -> 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 :: ATransport -> Spec
|
||||
testTiming (ATransport t) =
|
||||
it "should have similar time for auth error, whether queue exists or not, for all key sizes" $
|
||||
smpTest2 t $ \rh sh ->
|
||||
mapM_
|
||||
(testSameTiming rh sh)
|
||||
[ (128, 128, 100),
|
||||
(128, 256, 25),
|
||||
(128, 384, 15),
|
||||
-- (128, 512, 15),
|
||||
(256, 128, 100),
|
||||
(256, 256, 25),
|
||||
(256, 384, 15),
|
||||
-- (256, 512, 15),
|
||||
(384, 128, 100),
|
||||
(384, 256, 25),
|
||||
(384, 384, 15)
|
||||
-- (384, 512, 15),
|
||||
-- (512, 128, 100),
|
||||
-- (512, 256, 25),
|
||||
-- (512, 384, 15),
|
||||
-- (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 :: Transport c => THandle c -> THandle c -> (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 :: ATransport -> Spec
|
||||
syntaxTests (ATransport t) = do
|
||||
it "unknown command" $ ("", "abcd", "1234", "HELLO") >#> ("", "abcd", "1234", "ERR CMD SYNTAX")
|
||||
describe "NEW" do
|
||||
it "no parameters" $ ("1234", "bcda", "", "NEW") >#> ("", "bcda", "", "ERR SYNTAX 2")
|
||||
it "many parameters" $ ("1234", "cdab", "", "NEW 1 2") >#> ("", "cdab", "", "ERR SYNTAX 2")
|
||||
it "no signature" $ ("", "dabc", "", "NEW 3,1234,1234") >#> ("", "dabc", "", "ERR SYNTAX 3")
|
||||
it "queue ID" $ ("1234", "abcd", "12345678", "NEW 3,1234,1234") >#> ("", "abcd", "12345678", "ERR SYNTAX 4")
|
||||
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" $ ("1234", "bcda", "12345678", "KEY 3,4567,4567") >#> ("", "bcda", "12345678", "ERR AUTH")
|
||||
it "no parameters" $ ("1234", "cdab", "12345678", "KEY") >#> ("", "cdab", "12345678", "ERR SYNTAX 2")
|
||||
it "many parameters" $ ("1234", "dabc", "12345678", "KEY 1 2") >#> ("", "dabc", "12345678", "ERR SYNTAX 2")
|
||||
it "no signature" $ ("", "abcd", "12345678", "KEY 3,4567,4567") >#> ("", "abcd", "12345678", "ERR SYNTAX 3")
|
||||
it "no queue ID" $ ("1234", "bcda", "", "KEY 3,4567,4567") >#> ("", "bcda", "", "ERR SYNTAX 3")
|
||||
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" $ ("1234", "cdab", "12345678", "SEND 5\r\nhello") >#> ("", "cdab", "12345678", "ERR AUTH")
|
||||
it "valid syntax 2" $ ("1234", "dabc", "12345678", "SEND 11\r\nhello there") >#> ("", "dabc", "12345678", "ERR AUTH")
|
||||
it "no parameters" $ ("1234", "abcd", "12345678", "SEND") >#> ("", "abcd", "12345678", "ERR SYNTAX 2")
|
||||
it "no queue ID" $ ("1234", "bcda", "", "SEND 5\r\nhello") >#> ("", "bcda", "", "ERR SYNTAX 5")
|
||||
it "bad message body 1" $ ("1234", "cdab", "12345678", "SEND 11 hello") >#> ("", "cdab", "12345678", "ERR SYNTAX 2")
|
||||
it "bad message body 2" $ ("1234", "dabc", "12345678", "SEND hello") >#> ("", "dabc", "12345678", "ERR SYNTAX 2")
|
||||
it "bigger body" $ ("1234", "abcd", "12345678", "SEND 4\r\nhello") >#> ("", "abcd", "12345678", "ERR SIZE")
|
||||
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" $ ("1234", "bcda", "12345678", "OK") >#> ("", "bcda", "12345678", "ERR PROHIBITED")
|
||||
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" $ ("1234", "abcd", "12345678", cmd) >#> ("", "abcd", "12345678", "ERR AUTH")
|
||||
it "parameters" $ ("1234", "bcda", "12345678", cmd <> " 1") >#> ("", "bcda", "12345678", "ERR SYNTAX 2")
|
||||
it "no signature" $ ("", "cdab", "12345678", cmd) >#> ("", "cdab", "12345678", "ERR SYNTAX 3")
|
||||
it "no queue ID" $ ("1234", "dabc", "", cmd) >#> ("", "dabc", "", "ERR SYNTAX 3")
|
||||
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")
|
||||
(>#>) :: RawTransmission -> RawTransmission -> Expectation
|
||||
command >#> response = smpServerTest t command `shouldReturn` response
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
import AgentTests
|
||||
import AgentTests (agentTests)
|
||||
import ProtocolErrorTests
|
||||
import ServerTests
|
||||
import Simplex.Messaging.Transport (TCP, Transport (..))
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
|
||||
import Test.Hspec
|
||||
|
||||
main :: IO ()
|
||||
main = hspec do
|
||||
describe "SMP server" serverTests
|
||||
describe "SMP client agent" agentTests
|
||||
main = do
|
||||
createDirectoryIfMissing False "tests/tmp"
|
||||
hspec $ do
|
||||
describe "Protocol errors" protocolErrorTests
|
||||
describe "SMP server via TCP" $ serverTests (transport @TCP)
|
||||
describe "SMP server via WebSockets" $ serverTests (transport @WS)
|
||||
describe "SMP client agent" $ agentTests (transport @TCP)
|
||||
removeDirectoryRecursive "tests/tmp"
|
||||
|
||||