mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-01 05:08:22 +00:00
Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024304608e | ||
|
|
e3f5d244c1 | ||
|
|
f89d715a99 | ||
|
|
8fe18c4f6d | ||
|
|
2e5433676e | ||
|
|
8b21f7ef2a | ||
|
|
7a15ea59c9 | ||
|
|
1bb6a5c43b | ||
|
|
71489fe6fc | ||
|
|
1116aeeea1 | ||
|
|
077ca9c046 | ||
|
|
33f6d2f1da | ||
|
|
b33e8f4370 | ||
|
|
426f47c805 | ||
|
|
9af3297997 | ||
|
|
16d79da73e | ||
|
|
6d19033375 | ||
|
|
2d2cc86bd8 | ||
|
|
f51cf1deac | ||
|
|
00a2dde727 | ||
|
|
762909ce33 | ||
|
|
512afa1e2b | ||
|
|
c4c983348f | ||
|
|
969951d963 | ||
|
|
4455b8bd0e | ||
|
|
66c916dbb3 | ||
|
|
91cc48aabe | ||
|
|
8516b0dd5b | ||
|
|
9f72b249b4 | ||
|
|
b7afb725fd | ||
|
|
b48215d341 | ||
|
|
817fd77332 | ||
|
|
b27f126bab | ||
|
|
6d471b8be2 | ||
|
|
a70f492f4d | ||
|
|
c85f6a2f0e | ||
|
|
f22daf5cb6 | ||
|
|
fb5d54e0a2 | ||
|
|
6d60de2429 | ||
|
|
2d1609f222 | ||
|
|
58ede38bf4 | ||
|
|
7712070cb3 | ||
|
|
a3b229f668 | ||
|
|
8d7e0b1d09 | ||
|
|
ad4b5b6b71 | ||
|
|
98eb2742bc | ||
|
|
875ddd80d6 |
@@ -49,7 +49,7 @@ jobs:
|
||||
run: cabal build --enable-tests
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}}
|
||||
|
||||
- name: Build changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04'
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
with:
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' && matrix.ghc == '9.6.3'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.ghc != '8.10.7'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: |
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
common:
|
||||
corrId - random BS, used as CbNonce
|
||||
entityId - p2r tlsUniq
|
||||
|
||||
# setup
|
||||
s->p: "proxy", uri, auth?
|
||||
# unless connected
|
||||
p->r: "p_handshake"
|
||||
p<-r: "r_key", tls-signed dh pub
|
||||
s<-r: "r_key", tls-signed dh pub # reply entityId contains tlsUniq
|
||||
|
||||
# working
|
||||
s ; generate random dh priv, make shared secret
|
||||
s->p: s2r("forward", random dh pub, SEND command blob)
|
||||
p->r: p2r("forward", random dh pub, s2r("forward", ...)))
|
||||
r->c@ "msg", ...
|
||||
p<-r: p2r("r_res", s2r("ok" / "error", error))
|
||||
s<-p@ s2r("ok" / "error", error)
|
||||
|
||||
# expired
|
||||
p<-r@ p2r("error", "key expired")
|
||||
s<-p@ "error", "key expired"
|
||||
s ; reconnect
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 5.7.4.0
|
||||
version: 5.8.0.2
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
@@ -180,6 +180,7 @@ ghc-options:
|
||||
- -Wall
|
||||
- -Wcompat
|
||||
- -Werror=incomplete-patterns
|
||||
- -Werror=missing-methods
|
||||
- -Wredundant-constraints
|
||||
- -Wincomplete-record-updates
|
||||
- -Wincomplete-uni-patterns
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Problem
|
||||
|
||||
SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an audequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency.
|
||||
SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an adequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency.
|
||||
|
||||
The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realise that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product".
|
||||
The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realize that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product".
|
||||
|
||||
Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses.
|
||||
|
||||
@@ -43,7 +43,7 @@ Overall, this is not a viable or even appropriate option for the current stage.
|
||||
|
||||
3. SMP / XFTP proxy.
|
||||
|
||||
Introduce SMP and XFTP protocol extenstions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users.
|
||||
Introduce SMP and XFTP protocol extensions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users.
|
||||
|
||||
Pros:
|
||||
- no dependency on and lower latency than via Tor
|
||||
@@ -68,7 +68,7 @@ Below considers this design.
|
||||
|
||||
2. SMP proxy should not be able to observe queue addresses and their count on the destination relays. This requirement is not needed for XFTP proxies, as each file chunk is downloaded only once, so there is no need to hide its address.
|
||||
|
||||
3. There must be no identifiers and cyphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality).
|
||||
3. There must be no identifiers and ciphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality).
|
||||
|
||||
4. Traffic between the client and destination relays must be e2e encrypted, with MITM-by-proxy mitigated, relying on the relay identity (certificate fingerprint), ideally without any additional fingerprint in relay address.
|
||||
|
||||
@@ -97,11 +97,11 @@ This would also reduce the difference in how the traffic looks to the observer -
|
||||
|
||||
The flow of the messages will be:
|
||||
|
||||
1. Client requests proxy to create session with the relay by sending `server` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic.
|
||||
1. Client requests proxy to create session with the relay by sending `PRXY` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic.
|
||||
|
||||
2. Proxy connects to SMP relay, negotiating a shared secret in the handshake that will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). SMP relay also returns in handshake its temporary DH key to agree e2e encryption with the client (sender-relay encryption, to hide metadata sent to the destination relay from proxy).
|
||||
2. Proxy connects to SMP relay, negotiating a shared secret via a handshake headers - it will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). DH key returned by SMP relay in handshake will also be used to encrypt client commands, combining it with random per-command keys (sender-relay encryption, to hide metadata sent to the destination relay from proxy).
|
||||
|
||||
3. Proxy replies with `server_id` command including relay session ID to identify it in further requests, relay DH key for e2e encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before).
|
||||
3. Proxy replies to sender with `PKEY` message using "entityId" transmission field to indicate session ID for using in further requests, relay DH key for _s2r_ encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before).
|
||||
|
||||
A possible attack here is that proxy can use this TLS session to replay commands received from the client. Possibly, it could be mitigated with a bloom filter per proxy/SMP relay connection that would reject the repeated DH keys (that need to be used for replay), and also with DH key expiration (this mitigation should allow some acceptable rate of false positives from the bloom filter).
|
||||
|
||||
@@ -113,11 +113,11 @@ It is important that the same public key from destination relay is returned to a
|
||||
|
||||
*Unrelated cosideration for SMP protocol privacy improvement*: instead of signing commands to the destination relay, the sender could have a ratchet per queue agreed with the destination relay that would simply use authenticated encryption with per-message symmetric key to encrypt the message on the way to relay, and this encryption would be used as a proof of sender.
|
||||
|
||||
4. Now the client sends `forward` to proxy, which it then forwards to SMP relay, applying additional encryption layer.
|
||||
4. Now the client sends `PFWD` to proxy, which it then forwards to SMP relay as `RFWD`, applying _p2r_ encryption layer.
|
||||
|
||||
5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional encryption layer.
|
||||
5. SMP relay sends `RRES` to proxy applying _p2r_ encryption layer, which it then forwards to the client as `PRES`, removing the _p2r_ encryption layer.
|
||||
|
||||
Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session.
|
||||
Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients' relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session.
|
||||
|
||||
Sequence diagram for sending the message via SMP proxy:
|
||||
|
||||
@@ -126,33 +126,33 @@ Sequence diagram for sending the message via SMP proxy:
|
||||
| sending | | SMP | | SMP | | receiving |
|
||||
| client | | proxy | | relay | | client |
|
||||
------------- ------------- ------------- -------------
|
||||
| `server` | | |
|
||||
| -------------------------> | create TLS session, get keys | |
|
||||
| `PRXY` | | |
|
||||
| -------------------------> | | |
|
||||
| | ------------------------------> | |
|
||||
| `server_id` | (if doesn't exist) | |
|
||||
| | SMP handshake | |
|
||||
| | <------------------------------ | |
|
||||
| `PKEY` | | |
|
||||
| <------------------------- | | |
|
||||
| | | |
|
||||
| TLS(F:s2r(SEND(e2e(msg)))) | | |
|
||||
| -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | |
|
||||
| `PFWD` (s2r) | | |
|
||||
| -------------------------> | | |
|
||||
| | `RFWD` (p2r) | |
|
||||
| | ------------------------------> | |
|
||||
| | | |
|
||||
| | TLS(R:p2r(s2r(OK/ERR))) | |
|
||||
| TLS(R:s2r(OK/ERR)) | <------------------------------ | |
|
||||
| <------------------------- | | TLS(MSG(r2c(e2e(msg)))) |
|
||||
| | | -----------------------> |
|
||||
| | | |
|
||||
| | | TLS(ACK) |
|
||||
| | `RRES` (p2r) | |
|
||||
| | <------------------------------ | |
|
||||
| `PRES` (s2r) | | `MSG` |
|
||||
| <------------------------- | | -----------------------> |
|
||||
| | | `ACK` |
|
||||
| | | <----------------------- |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
```
|
||||
|
||||
Below diagram shows the encrypttion layers for `forward` and `response` commands:
|
||||
Below diagram shows the encrypttion layers for `PFWD`/`RFWD` commands and `RRES`/`PRES` responses:
|
||||
|
||||
- s2r (added) - encryption between client and SMP relay, with relay key returned in server_id command, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address.
|
||||
- s2r (added) - encryption between client and SMP relay, with relay key returned in relay handshake, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address.
|
||||
- e2e (exists now) - end-to-end encryption per SMP queue, with double ratchet e2e encryption inside it.
|
||||
- p2r (added) - additional encryption between proxy and SMP relay with key agreed in the handshake, to mitigate traffic correlation inside TLS. This key could also be signed by the same certificate, if we don't want to rely on TLS security.
|
||||
- p2r (added) - additional encryption between proxy and SMP relay with the shared secret agreed in the handshake, to mitigate traffic correlation inside TLS.
|
||||
- r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS.
|
||||
|
||||
```
|
||||
@@ -167,30 +167,85 @@ Below diagram shows the encrypttion layers for `forward` and `response` commands
|
||||
----------------- ----------------- -- TLS -- ----------------- -----------------
|
||||
```
|
||||
|
||||
When proxy connects to SMP relay it would indicate in the handshake that it will use proxy protocol and the SMP relay would expect the same `forward` commands and reply with `response`s.
|
||||
Question: should proxy declare its role in handshake? When proxy connects to SMP relay it would indicate in the handshake that it will act as a proxy and the SMP relay would expect the same `forward` commands and reply with `response`s.
|
||||
|
||||
Below syntax aims to fit in 16kb block using spare capacity in SMP protocol.
|
||||
Common SMP transmission format (v4), for reference:
|
||||
|
||||
```abnf
|
||||
proxy_block = padded(proxy_transmission, 16384)
|
||||
proxy_transmission = corr_id relay_session_id proxy_command
|
||||
corr_id = length *8 OCTET
|
||||
proxy_command = server / server_id / forward / response / error
|
||||
server = "S" address [relay_basic_auth] ; creates transport session between proxy and relay
|
||||
server_id = "I" relay_session_id tls_session_id signed_relay_key ;
|
||||
; session_id is the TLS session ID between proxy and relay, it has to be included inside encrypted block to prevent replay attacks
|
||||
forward = %s"F" random_dh_pub_key encrypted_block ; it's important that a new key is used for each command, to prevent any correlation by proxy or by destination relay
|
||||
response = %s"R" encrypted_block; response received from the destination SMP relay
|
||||
relay_session_id = length *8 OCTET
|
||||
error = %s"E" error
|
||||
paddedTransmission = <padded(transmission), 16384>
|
||||
transmission = signature signed
|
||||
signature = 0 ; empty signatures here
|
||||
signed = sessionIdentifier corrId entityId (smpCommand / brokerMsg)
|
||||
```
|
||||
|
||||
The overhead is: 1+8 (corrId) + 1+8 (relay_session_id) + 1 (command) + 1+32 (random_dh_pub_key) + 2 (original length) + 16 (auth tag for e2e encryption) + 16 (auth tag for proxy to relay encryption) = 86 bytes. The reserve for sent messages in SMP is ~84 bytes, so it should about fit with some reduced bytes somewhere.
|
||||
- `corrId` is fully random each time and used as a nonce for encrypted blocks.
|
||||
- `entityId` carries tlsUniq from the current proxy-to-relay connection.
|
||||
- `smpCommand` gets extended with `s2p_command / p2r_command`.
|
||||
- `brokerMsg` gets extended with `r_key / r_response`.
|
||||
|
||||
Another possible design is to allow mixing sent messages and normal SMP commands in the same transport connection, but it can make fitting in the block a bit harder, additional overhead would be: 1 (transmission count) + 2 (transmission size) + 1 (empty signature) = 4 bytes.
|
||||
```abnf
|
||||
s2p_command = proxy / forward
|
||||
p2r_command = p_handshake ; forward is
|
||||
proxy = %s"PRXY" SP relayUri SP basicAuth
|
||||
relayUri = length %s"smp://" serverIdentity "@" srvHost [":" port]
|
||||
forward = %s"PFWD" SP dhPublic SP encryptedBlock
|
||||
r_key = %s"PKEY" SP dhPublic
|
||||
r_response = %s"RRES" SP encryptedBlock
|
||||
dhPublic = length x509encoded
|
||||
```
|
||||
|
||||
The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response).
|
||||
|
||||
### Implementation considerations for the client
|
||||
|
||||
While client/server protocol is rather straightforward to implement, and it is already working, there are some decisions to make about how the client makes decisions about.
|
||||
|
||||
1. When to use proxy and when to connect directly to the destination relay.
|
||||
|
||||
While from the perspective of threat model improvement it may be beneficial to always use the proxy, choosing the proxy that is different from other relays in the connection, initially we need to make it opt-in, with an option to only use it for unknown destination relays, to minimize any unexpected adverse effect on the delivery latency.
|
||||
|
||||
Proxy mode will be passed from the client via NetworkConfig.
|
||||
|
||||
2. Which proxying relays to use.
|
||||
|
||||
Ability to request access to the session with the destination relay (and to create such session) is protected with the same basic auth approach as creating queues - the logic here is that opening private servers to all users as proxies would increase the scenarios for DoS attacks (which is the case with the public servers).
|
||||
|
||||
The open question is whether the client should choose proxies from:
|
||||
- all configured relays.
|
||||
- there should be a subset of configured relays.
|
||||
- there should be a separate list.
|
||||
|
||||
E.g., there could be a second toggle in the relay configuration to allow using relay as proxy, in addition to the current toggle that allows creating queues.
|
||||
|
||||
For simplicity, initially we will just use all enabled relays as potential proxies.
|
||||
|
||||
3. How many proxying relays should be used during one session.
|
||||
|
||||
This is not a simple question, and it creates a contradiction between two risks:
|
||||
- collusion between proxies and destination relays simplifies correlating sending clients by session - from the point of view of this risk, clients should follow the same policy for creating connections with proxies, that is to create a new connection for each user profile, and if transport isolation is set to "per connection" - for each destination queue.
|
||||
- traffic correlation by observable traffic sessions (particularly if an attacker can observe user's ISP traffic or multiple proxies) - from this point of view, it would be beneficial to use fewer proxies and fewer connections with proxies and see the risk of proxy colluding with the destination relay as lower than the risk of traffic observation that in the case of multiple sessions would allow to correlate traffic to rarely used destination relays (any private self-hosted relays) and the traffic of the user to a given proxy, to prove the fact of user communicating with the destination relay via the proxy.
|
||||
|
||||
While we can transfer this choice on the users, it seems a complex decision to make, and overall the second risk (traffic correlation) seems more important to address than the first.
|
||||
|
||||
In any case possible options are:
|
||||
1. Extreme option 1: Create a new proxy session, with the new random proxy, for each potential transport session that would exist if the user were to be connected to destination relays directly. That is, never to mix access to multiple relays from multiple user profiles (and in case of per-connection isolation, to multiple queues) into a one client session with proxy. This is a rather radical option that nullifies any advantages of having fewer sessions with proxies than there would have been with the destination relays and removes any benefits of batching destination server session requests (PRXY comands).
|
||||
2. Extreme option 2: Use only one proxy session at the time, mixing traffic from all user profiles and to all destination servers (and for all queues) into a session with one proxy. This minimizes the risks of traffic correlation in case of non-colluding proxy, but maximises the risk in case it colludes with the destination relays.
|
||||
3. Balanced option: Use one proxy session per user profile, but mix traffic to multiple queues irrespective of connection isolation option and to all destination servers. Given that connection isolation is an experimental option, this makes the most sense, but it would have to be disclosed.
|
||||
4. Less balanced option: take connection isolation option into account and create a new proxy connection for each destination queue. This feels worse than option 3.
|
||||
|
||||
If option 3 is chosen, then the transport session key with the proxy would be different from the transport session key with the relay - proxy session will only use UserId as the key, and the relay session uses (UserId, Server, Maybe EntityId) as the key.
|
||||
|
||||
If option 4 is chosen, the keys would also be different, as the proxy would then use (UserId, Maybe (Server, EntityId)) as the key.
|
||||
|
||||
We could potentially key proxy sessions (and create proxy connections) per each destination relay, in the same way as we key relays themselves, but it seems to have the least sense, as we neither achieve isolation by queue in case proxy and destination relay collude, nor we sufficiently protect from traffic correlation by any observers.
|
||||
|
||||
The implemented design is this:
|
||||
- for each destination relay a random proxy is chosen and used to send all messages - all requests from a client coalesce to a single session.
|
||||
- transport isolation mode is taken into account, that is if per-connection isolation is enabled, then a separate proxy connection will be created for each messaging queue.
|
||||
- supported modes when proxy is used: always, for unknown relays, for unknown relays when IP address is not protected, never.
|
||||
|
||||
This decision is made because the argument for protection against collusion between proxy and relay and more balanced traffic distribution is stronger than the argument for protection against traffic correlation, because even mixing all messages to one proxy connection does not provide protection against traffic correlation by time, so in any case it requires adding delays.
|
||||
|
||||
### Threat model for SMP proxy and changes to threat model for SMP
|
||||
|
||||
#### SMP proxy
|
||||
|
||||
+10
-8
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 5.7.4.0
|
||||
version: 5.8.0.2
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -105,6 +105,7 @@ library
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
@@ -174,7 +175,7 @@ library
|
||||
src
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2
|
||||
include-dirs:
|
||||
cbits
|
||||
c-sources:
|
||||
@@ -255,7 +256,7 @@ executable ntf-server
|
||||
apps/ntf-server
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
@@ -330,7 +331,7 @@ executable smp-agent
|
||||
apps/smp-agent
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
@@ -405,7 +406,7 @@ executable smp-server
|
||||
apps/smp-server
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
@@ -480,7 +481,7 @@ executable xftp
|
||||
apps/xftp
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
@@ -555,7 +556,7 @@ executable xftp-server
|
||||
apps/xftp-server
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
@@ -652,6 +653,7 @@ test-suite simplexmq-test
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
SMPClient
|
||||
SMPProxyTests
|
||||
Util
|
||||
XFTPAgent
|
||||
XFTPCLI
|
||||
@@ -662,7 +664,7 @@ test-suite simplexmq-test
|
||||
tests
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Werror=missing-methods -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
|
||||
build-depends:
|
||||
HUnit ==1.6.*
|
||||
, QuickCheck ==2.14.*
|
||||
|
||||
@@ -28,7 +28,6 @@ module Simplex.FileTransfer.Agent
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
@@ -112,8 +111,8 @@ closeXFTPAgent a = do
|
||||
where
|
||||
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
|
||||
|
||||
xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AM RcvFileId
|
||||
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs = do
|
||||
xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AM RcvFileId
|
||||
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs approvedRelays = do
|
||||
g <- asks random
|
||||
prefixPath <- lift $ getPrefixPath "rcv.xftp"
|
||||
createDirectory prefixPath
|
||||
@@ -124,7 +123,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi
|
||||
lift $ createEmptyFile =<< toFSFilePath relSavePath
|
||||
let saveFile = CryptoFile relSavePath cfArgs
|
||||
fId <- case redirect of
|
||||
Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile
|
||||
Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile approvedRelays
|
||||
Just _ -> do
|
||||
-- prepare description paths
|
||||
let relTmpPathRedirect = relPrefixPath </> "xftp.redirect-encrypted"
|
||||
@@ -134,7 +133,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi
|
||||
cfArgsRedirect <- atomically $ CF.randomArgs g
|
||||
let saveFileRedirect = CryptoFile relSavePathRedirect $ Just cfArgsRedirect
|
||||
-- create download tasks
|
||||
withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile
|
||||
withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile approvedRelays
|
||||
forM_ chunks (downloadChunk c)
|
||||
pure fId
|
||||
|
||||
@@ -176,13 +175,13 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} =
|
||||
withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case
|
||||
RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []} -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) "chunk has no replicas"
|
||||
fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do
|
||||
(RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _) -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (INTERNAL "chunk has no replicas")
|
||||
(fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays) -> do
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
downloadFileChunk fc replica
|
||||
`catchAgentError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e
|
||||
liftIO $ waitForUserNetwork c
|
||||
downloadFileChunk fc replica approvedRelays
|
||||
`catchAgentError` \e -> retryOnError c "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
@@ -191,9 +190,10 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (show e)
|
||||
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> AM ()
|
||||
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do
|
||||
retryDone = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath)
|
||||
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM ()
|
||||
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do
|
||||
unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwError $ XFTP "" XFTP.NOT_APPROVED
|
||||
fsFileTmpPath <- lift $ toFSFilePath fileTmpPath
|
||||
chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
@@ -214,6 +214,10 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
when complete . lift . void $
|
||||
getXFTPRcvWorker True c Nothing
|
||||
where
|
||||
ipAddressProtected' :: AM Bool
|
||||
ipAddressProtected' = do
|
||||
cfg <- liftIO $ getNetworkConfig' c
|
||||
pure $ ipAddressProtected cfg srv
|
||||
receivedSize :: [RcvFileChunk] -> Int64
|
||||
receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0
|
||||
receivedChunkSize ch@RcvFileChunk {chunkSize = s}
|
||||
@@ -227,18 +231,16 @@ withRetryIntervalLimit maxN ri action =
|
||||
withRetryIntervalCount ri $ \n delay loop ->
|
||||
when (n < maxN) $ action delay loop
|
||||
|
||||
retryOnError :: Text -> AM a -> AM a -> AgentErrorType -> AM a
|
||||
retryOnError name loop done e = do
|
||||
logError $ name <> " error: " <> tshow e
|
||||
if temporaryAgentError e
|
||||
then loop
|
||||
else done
|
||||
retryOnError :: AgentClient -> Text -> AM a -> AM a -> AgentErrorType -> AM a
|
||||
retryOnError c name loop done e = do
|
||||
logWarn c $ name <> " error: " <> tshow e
|
||||
if temporaryAgentError e then loop else done
|
||||
|
||||
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> AM ()
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath internalErrStr = do
|
||||
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath err = do
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr
|
||||
notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr
|
||||
withStore' c $ \db -> updateRcvFileError db rcvFileId (show err)
|
||||
notify c rcvFileEntityId $ RFERR err
|
||||
|
||||
runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM ()
|
||||
runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
@@ -252,7 +254,7 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
runXFTPOperation AgentConfig {rcvFilesTTL} =
|
||||
withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $
|
||||
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
|
||||
decryptFile f `catchAgentError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show)
|
||||
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath
|
||||
decryptFile :: RcvFile -> AM ()
|
||||
decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do
|
||||
let CryptoFile savePath cfArgs = saveFile
|
||||
@@ -262,9 +264,9 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting
|
||||
chunkPaths <- getChunkPaths chunks
|
||||
encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths
|
||||
when (FileSize encSize /= size) $ throwError $ XFTP XFTP.SIZE
|
||||
when (FileSize encSize /= size) $ throwError $ XFTP "" XFTP.SIZE
|
||||
encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths
|
||||
when (FileDigest encDigest /= digest) $ throwError $ XFTP XFTP.DIGEST
|
||||
when (FileDigest encDigest /= digest) $ throwError $ XFTP "" XFTP.DIGEST
|
||||
let destFile = CryptoFile fsSavePath cfArgs
|
||||
void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile
|
||||
case redirect of
|
||||
@@ -281,10 +283,11 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
-- proceed with redirect
|
||||
yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath)
|
||||
next@FileDescription {chunks = nextChunks} <- case strDecode (LB.toStrict yaml) of
|
||||
Left _ -> throwError . XFTP $ XFTP.REDIRECT "decode error"
|
||||
-- TODO switch to another error constructor
|
||||
Left _ -> throwError . XFTP "" $ XFTP.REDIRECT "decode error"
|
||||
Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest})
|
||||
| dstSize /= redirectSize -> throwError . XFTP $ XFTP.REDIRECT "size mismatch"
|
||||
| dstDigest /= redirectDigest -> throwError . XFTP $ XFTP.REDIRECT "digest mismatch"
|
||||
| dstSize /= redirectSize -> throwError . XFTP "" $ XFTP.REDIRECT "size mismatch"
|
||||
| dstDigest /= redirectDigest -> throwError . XFTP "" $ XFTP.REDIRECT "digest mismatch"
|
||||
| otherwise -> pure fd
|
||||
-- register and download chunks from the actual file
|
||||
withStore c $ \db -> updateRcvFileRedirect db redirectDbId next
|
||||
@@ -424,9 +427,9 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
tryCreate = do
|
||||
usedSrvs <- newTVarIO ([] :: [XFTPServer])
|
||||
withRetryInterval (riFast ri) $ \_ loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
liftIO $ waitForUserNetwork c
|
||||
createWithNextSrv usedSrvs
|
||||
`catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e
|
||||
`catchAgentError` \e -> retryOnError c "XFTP prepare worker" (retryLoop loop) (throwError e) e
|
||||
where
|
||||
retryLoop loop = atomically (assertAgentForeground c) >> loop
|
||||
createWithNextSrv usedSrvs = do
|
||||
@@ -457,9 +460,9 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
liftIO $ waitForUserNetwork c
|
||||
uploadFileChunk cfg fc replica
|
||||
`catchAgentError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e
|
||||
`catchAgentError` \e -> retryOnError c "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
@@ -624,9 +627,9 @@ runXFTPDelWorker c srv Worker {doWork} = do
|
||||
processDeletedReplica replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} = do
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
liftIO $ waitForUserNetwork c
|
||||
deleteChunkReplica
|
||||
`catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e
|
||||
`catchAgentError` \e -> retryOnError c "XFTP del worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
|
||||
@@ -14,6 +14,7 @@ module Simplex.FileTransfer.Client where
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
@@ -38,6 +39,7 @@ import Simplex.Messaging.Client
|
||||
defaultNetworkConfig,
|
||||
proxyUsername,
|
||||
transportClientConfig,
|
||||
unexpectedResponse,
|
||||
)
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -51,13 +53,13 @@ import Simplex.Messaging.Protocol
|
||||
RecipientId,
|
||||
SenderId,
|
||||
)
|
||||
import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
|
||||
import Simplex.Messaging.Transport (ALPN, THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM)
|
||||
import Simplex.Messaging.Version (compatibleVersion, pattern Compatible)
|
||||
import Simplex.Messaging.Util (liftEitherWith, liftError', tshow, whenM)
|
||||
import Simplex.Messaging.Version
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
@@ -99,22 +101,24 @@ defaultXFTPClientConfig =
|
||||
|
||||
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
|
||||
let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = clientALPN}
|
||||
http2Config = xftpHTTP2Config tcConfig config
|
||||
username = proxyUsername transportSession
|
||||
let username = proxyUsername transportSession
|
||||
ProtocolServer _ host port keyHash = srv
|
||||
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
|
||||
let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN}
|
||||
http2Config = xftpHTTP2Config tcConfig config
|
||||
clientVar <- newTVarIO Nothing
|
||||
let usePort = if null port then "443" else port
|
||||
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
|
||||
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
|
||||
let HTTP2Client {sessionId, sessionALPN} = http2Client
|
||||
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True}
|
||||
v = VersionXFTP 1
|
||||
thServerVRange = versionToRange v
|
||||
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True}
|
||||
logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN
|
||||
thParams@THandleParams {thVersion} <- case sessionALPN of
|
||||
Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0
|
||||
Nothing -> pure thParams0
|
||||
_ -> throwError $ PCETransportError (TEHandshake VERSION)
|
||||
_ -> throwError $ PCETransportError TEVersion
|
||||
logDebug $ "Client negotiated protocol: " <> tshow thVersion
|
||||
let c = XFTPClient {http2Client, thParams, transportSession, config}
|
||||
atomically $ writeTVar clientVar $ Just c
|
||||
@@ -123,9 +127,10 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN,
|
||||
xftpClientHandshakeV1 :: VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient)
|
||||
xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do
|
||||
shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake
|
||||
(v, sk) <- processServerHandshake shs
|
||||
(vr, sk) <- processServerHandshake shs
|
||||
let v = maxVersion vr
|
||||
sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash}
|
||||
pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v}
|
||||
pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v, thServerVRange = vr}
|
||||
where
|
||||
getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake
|
||||
getServerHandshake = do
|
||||
@@ -133,13 +138,13 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session
|
||||
HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <-
|
||||
liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing
|
||||
liftHS . smpDecode =<< liftHS (C.unPad shsBody)
|
||||
processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519)
|
||||
processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519)
|
||||
processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do
|
||||
unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION
|
||||
case xftpVersionRange `compatibleVersion` serverVRange of
|
||||
Nothing -> throwError $ PCEResponseError HANDSHAKE
|
||||
Just (Compatible v) ->
|
||||
fmap (v,) . liftHS $ do
|
||||
case xftpVersionRange `compatibleVRange` serverVRange of
|
||||
Nothing -> throwError $ PCETransportError TEVersion
|
||||
Just (Compatible vr) ->
|
||||
fmap (vr,) . liftHS $ do
|
||||
let (X.CertificateChain cert, exact) = serverAuth
|
||||
case cert of
|
||||
[_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure ()
|
||||
@@ -185,9 +190,11 @@ xftpClientError = \case
|
||||
|
||||
sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
|
||||
sendXFTPCommand c@XFTPClient {thParams} pKey fId cmd chunkSpec_ = do
|
||||
-- TODO random corrId
|
||||
let corrIdUsedAsNonce = ""
|
||||
t <-
|
||||
liftEither . first PCETransportError $
|
||||
xftpEncodeAuthTransmission thParams pKey ("", fId, FileCmd (sFileParty @p) cmd)
|
||||
xftpEncodeAuthTransmission thParams pKey (corrIdUsedAsNonce, fId, FileCmd (sFileParty @p) cmd)
|
||||
sendXFTPTransmission c t chunkSpec_
|
||||
|
||||
sendXFTPTransmission :: XFTPClient -> ByteString -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
|
||||
@@ -223,13 +230,13 @@ createXFTPChunk ::
|
||||
createXFTPChunk c spKey file rcps auth_ =
|
||||
sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case
|
||||
(FRSndIds sId rIds, body) -> noFile body (sId, rIds)
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
(r, _) -> throwE $ unexpectedResponse r
|
||||
|
||||
addXFTPRecipients :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> NonEmpty C.APublicAuthKey -> ExceptT XFTPClientError IO (NonEmpty RecipientId)
|
||||
addXFTPRecipients c spKey fId rcps =
|
||||
sendXFTPCommand c spKey fId (FADD rcps) Nothing >>= \case
|
||||
(FRRcvIds rIds, body) -> noFile body rIds
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
(r, _) -> throwE $ unexpectedResponse r
|
||||
|
||||
uploadXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> XFTPChunkSpec -> ExceptT XFTPClientError IO ()
|
||||
uploadXFTPChunk c spKey fId chunkSpec =
|
||||
@@ -257,7 +264,7 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {
|
||||
receiveEncFile chunkPart cbState chunkSpec `catchError` \e ->
|
||||
whenM (doesFileExist filePath) (removeFile filePath) >> throwError e
|
||||
_ -> throwError $ PCEResponseError NO_FILE
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
(r, _) -> throwE $ unexpectedResponse r
|
||||
|
||||
xftpReqTimeout :: XFTPClientConfig -> Maybe Word32 -> Int
|
||||
xftpReqTimeout cfg@XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout}} chunkSize_ =
|
||||
@@ -281,12 +288,12 @@ pingXFTP c@XFTPClient {thParams} = do
|
||||
(r, _) <- sendXFTPTransmission c t Nothing
|
||||
case r of
|
||||
FRPong -> pure ()
|
||||
_ -> throwError $ PCEUnexpectedResponse $ bshow r
|
||||
_ -> throwE $ unexpectedResponse r
|
||||
|
||||
okResponse :: (FileResponse, HTTP2Body) -> ExceptT XFTPClientError IO ()
|
||||
okResponse = \case
|
||||
(FROk, body) -> noFile body ()
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
(r, _) -> throwE $ unexpectedResponse r
|
||||
|
||||
-- TODO this currently does not check anything because response size is not set and bodyPart is always Just
|
||||
noFile :: HTTP2Body -> a -> ExceptT XFTPClientError IO a
|
||||
|
||||
@@ -48,6 +48,7 @@ import Simplex.Messaging.Protocol
|
||||
SndPublicAuthKey,
|
||||
Transmission,
|
||||
TransmissionForAuth (..),
|
||||
CorrId (..),
|
||||
encodeTransmission,
|
||||
encodeTransmissionForAuth,
|
||||
messageTagP,
|
||||
@@ -328,7 +329,7 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of
|
||||
xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString
|
||||
xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg)
|
||||
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth
|
||||
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) (C.cbNonce $ bs corrId) tForAuth
|
||||
|
||||
xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString
|
||||
xftpEncodeTransmission thParams (corrId, fId, msg) = do
|
||||
|
||||
@@ -63,7 +63,7 @@ import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize)
|
||||
import Simplex.Messaging.Transport.HTTP2.Server
|
||||
import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version (isCompatible)
|
||||
import Simplex.Messaging.Version
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath ((</>))
|
||||
import System.IO (hPrint, hPutStrLn, universalNewlineMode)
|
||||
@@ -91,10 +91,10 @@ runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpSer
|
||||
|
||||
data Handshake
|
||||
= HandshakeSent C.PrivateKeyX25519
|
||||
| HandshakeAccepted (THandleAuth 'TServer) VersionXFTP
|
||||
| HandshakeAccepted (THandleParams XFTPVersion 'TServer)
|
||||
|
||||
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do
|
||||
mapM_ (expireServerFiles Nothing) fileExpiration
|
||||
restoreServerStats
|
||||
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer
|
||||
@@ -111,7 +111,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
let cleanup sessionId = atomically $ TM.delete sessionId sessions
|
||||
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r xftpBlockSize
|
||||
let thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True}
|
||||
let v = VersionXFTP 1
|
||||
thServerVRange = versionToRange v
|
||||
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True}
|
||||
req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse}
|
||||
flip runReaderT env $ case sessionALPN of
|
||||
Nothing -> processRequest req0
|
||||
@@ -121,12 +123,12 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here)
|
||||
_ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about
|
||||
xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer))
|
||||
xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do
|
||||
xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams0@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do
|
||||
s <- atomically $ TM.lookup sessionId sessions
|
||||
r <- runExceptT $ case s of
|
||||
Nothing -> processHello
|
||||
Just (HandshakeSent pk) -> processClientHandshake pk
|
||||
Just (HandshakeAccepted auth v) -> pure $ Just thParams {thAuth = Just auth, thVersion = v}
|
||||
Just (HandshakeAccepted thParams) -> pure $ Just thParams
|
||||
either sendError pure r
|
||||
where
|
||||
processHello = do
|
||||
@@ -134,21 +136,24 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
(k, pk) <- atomically . C.generateKeyPair =<< asks random
|
||||
atomically $ TM.insert sessionId (HandshakeSent pk) sessions
|
||||
let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k)
|
||||
let hs = XFTPServerHandshake {xftpVersionRange = supportedFileServerVRange, sessionId, authPubKey}
|
||||
let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey}
|
||||
shs <- encodeXftp hs
|
||||
liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs
|
||||
pure Nothing
|
||||
processClientHandshake pk = do
|
||||
unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE
|
||||
body <- liftHS $ C.unPad bodyHead
|
||||
XFTPClientHandshake {xftpVersion, keyHash} <- liftHS $ smpDecode body
|
||||
XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ smpDecode body
|
||||
kh <- asks serverIdentity
|
||||
unless (keyHash == kh) $ throwError HANDSHAKE
|
||||
unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE
|
||||
let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing}
|
||||
atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions
|
||||
liftIO . sendResponse $ H.responseNoBody N.ok200 []
|
||||
pure Nothing
|
||||
case compatibleVRange' xftpServerVRange v of
|
||||
Just (Compatible vr) -> do
|
||||
let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing}
|
||||
thParams = thParams0 {thAuth = Just auth, thVersion = v, thServerVRange = vr}
|
||||
atomically $ TM.insert sessionId (HandshakeAccepted thParams) sessions
|
||||
liftIO . sendResponse $ H.responseNoBody N.ok200 []
|
||||
pure Nothing
|
||||
Nothing -> throwError HANDSHAKE
|
||||
sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer))
|
||||
sendError err = do
|
||||
runExceptT (encodeXftp err) >>= \case
|
||||
|
||||
@@ -25,6 +25,7 @@ import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId)
|
||||
import Simplex.FileTransfer.Server.Stats
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.FileTransfer.Server.StoreLog
|
||||
import Simplex.FileTransfer.Transport (VersionRangeXFTP)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
@@ -61,6 +62,8 @@ data XFTPServerConfig = XFTPServerConfig
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
-- | XFTP client-server protocol version range
|
||||
xftpServerVRange :: VersionRangeXFTP,
|
||||
-- stats config - see SMP server config
|
||||
logStatsInterval :: Maybe Int64,
|
||||
logStatsStartTime :: Int64,
|
||||
|
||||
@@ -20,6 +20,7 @@ import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Description (FileSize (..))
|
||||
import Simplex.FileTransfer.Server (runXFTPServer)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes)
|
||||
import Simplex.FileTransfer.Transport (supportedFileServerVRange)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern XFTPServer)
|
||||
@@ -174,6 +175,7 @@ xftpServerCLI cfgPath logPath = do
|
||||
caCertificateFile = c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile,
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
logStatsInterval = logStats $> 86400, -- seconds
|
||||
logStatsStartTime = 0, -- seconds from 00:00 UTC
|
||||
serverStatsLogFile = combine logPath "file-server-stats.daily.log",
|
||||
|
||||
@@ -37,6 +37,7 @@ import qualified Control.Exception as E
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Trans.Except
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (bimap, first)
|
||||
@@ -53,7 +54,7 @@ import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol (CommandError)
|
||||
import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport (SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import Simplex.Messaging.Version
|
||||
@@ -95,7 +96,7 @@ supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion
|
||||
|
||||
-- XFTP protocol does not use this handshake method
|
||||
xftpClientHandshakeStub :: c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient)
|
||||
xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION
|
||||
xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwE TEVersion
|
||||
|
||||
data XFTPServerHandshake = XFTPServerHandshake
|
||||
{ xftpVersionRange :: VersionRangeXFTP,
|
||||
@@ -216,6 +217,8 @@ data XFTPErrorType
|
||||
TIMEOUT
|
||||
| -- | bad redirect data
|
||||
REDIRECT {redirectError :: String}
|
||||
| -- | cannot proceed with download from not approved relays without proxy
|
||||
NOT_APPROVED
|
||||
| -- | internal server error
|
||||
INTERNAL
|
||||
| -- | used internally, never returned by the server (to be removed)
|
||||
@@ -248,6 +251,7 @@ instance Encoding XFTPErrorType where
|
||||
FILE_IO -> "FILE_IO"
|
||||
TIMEOUT -> "TIMEOUT"
|
||||
REDIRECT err -> "REDIRECT " <> smpEncode err
|
||||
NOT_APPROVED -> "NOT_APPROVED"
|
||||
INTERNAL -> "INTERNAL"
|
||||
DUPLICATE_ -> "DUPLICATE_"
|
||||
|
||||
@@ -267,6 +271,7 @@ instance Encoding XFTPErrorType where
|
||||
"FILE_IO" -> pure FILE_IO
|
||||
"TIMEOUT" -> pure TIMEOUT
|
||||
"REDIRECT" -> REDIRECT <$> _smpP
|
||||
"NOT_APPROVED" -> pure NOT_APPROVED
|
||||
"INTERNAL" -> pure INTERNAL
|
||||
"DUPLICATE_" -> pure DUPLICATE_
|
||||
_ -> fail "bad error type"
|
||||
|
||||
+144
-80
@@ -83,6 +83,7 @@ module Simplex.Messaging.Agent
|
||||
setNtfServers,
|
||||
setNetworkConfig,
|
||||
getNetworkConfig,
|
||||
getNetworkConfig',
|
||||
setUserNetworkInfo,
|
||||
reconnectAllServers,
|
||||
registerNtfToken,
|
||||
@@ -113,12 +114,13 @@ module Simplex.Messaging.Agent
|
||||
debugAgentLocks,
|
||||
getAgentStats,
|
||||
resetAgentStats,
|
||||
getMsgCounts,
|
||||
setAgentLogLevel,
|
||||
getAgentSubscriptions,
|
||||
logConnection,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple (logError, logInfo, showText)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
@@ -158,7 +160,7 @@ import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission)
|
||||
import Simplex.Messaging.Client (ProtocolClient (..), SMPClientError, ServerTransmission (..), ServerTransmissionBatch, temporaryClientError, unexpectedResponse)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs)
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
||||
@@ -169,7 +171,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..))
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Parsers (parse)
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -211,9 +213,9 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode =
|
||||
run c "runNtfSupervisor" $ runNtfSupervisor c,
|
||||
run c "cleanupManager" $ cleanupManager c
|
||||
]
|
||||
run AgentClient {subQ, acThread} name a =
|
||||
run c@AgentClient {subQ, acThread} name a =
|
||||
a `E.catchAny` \e -> whenM (isJust <$> readTVarIO acThread) $ do
|
||||
logError $ "Agent thread " <> name <> " crashed: " <> tshow e
|
||||
logError c $ "Agent thread " <> name <> " crashed: " <> tshow e
|
||||
atomically $ writeTBQueue subQ ("", "", APC SAEConn $ ERR $ CRITICAL True $ show e)
|
||||
|
||||
disconnectAgentClient :: AgentClient -> IO ()
|
||||
@@ -425,26 +427,25 @@ setNetworkConfig c@AgentClient {useNetworkConfig} cfg' = do
|
||||
|
||||
-- returns fast network config
|
||||
getNetworkConfig :: AgentClient -> IO NetworkConfig
|
||||
getNetworkConfig = fmap snd . readTVarIO . useNetworkConfig
|
||||
getNetworkConfig = getNetworkConfig'
|
||||
{-# INLINE getNetworkConfig #-}
|
||||
|
||||
setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO ()
|
||||
setUserNetworkInfo c@AgentClient {userNetworkState} UserNetworkInfo {networkType = nt', online} = withAgentEnv' c $ do
|
||||
d <- asks $ initialInterval . userNetworkInterval . config
|
||||
ts <- liftIO getCurrentTime
|
||||
atomically $ do
|
||||
ns@UserNetworkState {networkType = nt, offline} <- readTVar userNetworkState
|
||||
when (nt' /= nt || online /= isNothing offline) $
|
||||
writeTVar userNetworkState $!
|
||||
let offline'
|
||||
| nt' /= UNNone && online = Nothing
|
||||
| isJust offline = offline
|
||||
| otherwise = Just UNSOffline {offlineDelay = d, offlineFrom = ts}
|
||||
in ns {networkType = nt', offline = offline'}
|
||||
setUserNetworkInfo c@AgentClient {userNetworkInfo, userNetworkUpdated} ni = withAgentEnv' c $ do
|
||||
ts' <- liftIO getCurrentTime
|
||||
i <- asks $ userOfflineDelay . config
|
||||
-- if network offline event happens in less than `userOfflineDelay` after the previous event, it is ignored
|
||||
atomically . whenM ((isOnline ni ||) <$> notRecentlyChanged ts' i) $ do
|
||||
writeTVar userNetworkInfo ni
|
||||
writeTVar userNetworkUpdated $ Just ts'
|
||||
where
|
||||
notRecentlyChanged ts' i =
|
||||
maybe True (\ts -> diffUTCTime ts' ts > i) <$> readTVar userNetworkUpdated
|
||||
|
||||
reconnectAllServers :: AgentClient -> IO ()
|
||||
reconnectAllServers c = do
|
||||
reconnectServerClients c smpClients
|
||||
reconnectServerClients c xftpClients
|
||||
reconnectServerClients c ntfClients
|
||||
|
||||
-- | Register device notifications token
|
||||
@@ -483,8 +484,8 @@ xftpStartWorkers c = withAgentEnv c . startXFTPWorkers c
|
||||
{-# INLINE xftpStartWorkers #-}
|
||||
|
||||
-- | Receive XFTP file
|
||||
xftpReceiveFile :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AE RcvFileId
|
||||
xftpReceiveFile c = withAgentEnv c .:. xftpReceiveFile' c
|
||||
xftpReceiveFile :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AE RcvFileId
|
||||
xftpReceiveFile c = withAgentEnv c .:: xftpReceiveFile' c
|
||||
{-# INLINE xftpReceiveFile #-}
|
||||
|
||||
-- | Delete XFTP rcv file (deletes work files from file system and db records)
|
||||
@@ -554,6 +555,9 @@ resetAgentStats :: AgentClient -> IO ()
|
||||
resetAgentStats = atomically . TM.clear . agentStats
|
||||
{-# INLINE resetAgentStats #-}
|
||||
|
||||
getMsgCounts :: AgentClient -> IO [(ConnId, (Int, Int))] -- (total, duplicates)
|
||||
getMsgCounts c = readTVarIO (msgCounts c) >>= mapM (\(connId, cnt) -> (connId,) <$> readTVarIO cnt) . M.assocs
|
||||
|
||||
withAgentEnv' :: AgentClient -> AM' a -> IO a
|
||||
withAgentEnv' c = (`runReaderT` agentEnv c)
|
||||
{-# INLINE withAgentEnv' #-}
|
||||
@@ -565,7 +569,7 @@ withAgentEnv c a = ExceptT $ runExceptT a `runReaderT` agentEnv c
|
||||
logConnection :: AgentClient -> Bool -> IO ()
|
||||
logConnection c connected =
|
||||
let event = if connected then "connected to" else "disconnected from"
|
||||
in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"]
|
||||
in logInfo c $ T.unwords ["client", tshow (clientId c), event, "Agent"]
|
||||
|
||||
-- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's.
|
||||
runAgentClient :: AgentClient -> AM' ()
|
||||
@@ -734,11 +738,9 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv
|
||||
(SCMContact, CR.IKUsePQ) -> throwError $ CMD PROHIBITED
|
||||
_ -> pure ()
|
||||
AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config
|
||||
(rq, qUri) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e
|
||||
(rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e
|
||||
rq' <- withStore c $ \db -> updateNewConnRcv db connId rq
|
||||
liftIO $ case subMode of
|
||||
SMOnlyCreate -> pure ()
|
||||
SMSubscribe -> addSubscription c rq'
|
||||
lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId
|
||||
when enableNtfs $ do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCCreate)
|
||||
@@ -847,7 +849,7 @@ joinConnSrv c userId connId hasNewConn enableNtfs cReqUri@CRContactUri {} cInfo
|
||||
lift (compatibleContactUri cReqUri) >>= \case
|
||||
Just (qInfo, vrsn) -> do
|
||||
(connId', cReq) <- newConnSrv c userId connId hasNewConn enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv
|
||||
sendInvitation c userId qInfo vrsn cReq cInfo
|
||||
void $ sendInvitation c userId qInfo vrsn cReq cInfo
|
||||
pure connId'
|
||||
Nothing -> throwError $ AGENT A_VERSION
|
||||
|
||||
@@ -863,12 +865,10 @@ joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode
|
||||
|
||||
createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo
|
||||
createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do
|
||||
(rq, qUri) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode
|
||||
(rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode
|
||||
let qInfo = toVersionT qUri smpClientVersion
|
||||
rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq
|
||||
liftIO $ case subMode of
|
||||
SMOnlyCreate -> pure ()
|
||||
SMSubscribe -> addSubscription c rq'
|
||||
lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId
|
||||
when enableNtfs $ do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCCreate)
|
||||
@@ -908,12 +908,12 @@ rejectContact' c contactConnId invId =
|
||||
|
||||
-- | Subscribe to receive connection messages (SUB command) in Reader monad
|
||||
subscribeConnection' :: AgentClient -> ConnId -> AM ()
|
||||
subscribeConnection' c connId = toConnResult connId =<< subscribeConnections' c [connId]
|
||||
subscribeConnection' c connId = toConnResult c connId =<< subscribeConnections' c [connId]
|
||||
{-# INLINE subscribeConnection' #-}
|
||||
|
||||
toConnResult :: ConnId -> Map ConnId (Either AgentErrorType ()) -> AM ()
|
||||
toConnResult connId rs = case M.lookup connId rs of
|
||||
Just (Right ()) -> when (M.size rs > 1) $ logError $ T.pack $ "too many results " <> show (M.size rs)
|
||||
toConnResult :: AgentClient -> ConnId -> Map ConnId (Either AgentErrorType ()) -> AM ()
|
||||
toConnResult c connId rs = case M.lookup connId rs of
|
||||
Just (Right ()) -> when (M.size rs > 1) $ logError c $ T.pack $ "too many results " <> show (M.size rs)
|
||||
Just (Left e) -> throwError e
|
||||
_ -> throwError $ INTERNAL $ "no result for connection " <> B.unpack connId
|
||||
|
||||
@@ -928,7 +928,7 @@ subscribeConnections' c connIds = do
|
||||
(subRs, rcvQs) = M.mapEither rcvQueueOrResult cs
|
||||
mapM_ (mapM_ (\(cData, sqs) -> mapM_ (lift . resumeMsgDelivery c cData) sqs) . sndQueue) cs
|
||||
mapM_ (resumeConnCmds c) $ M.keys cs
|
||||
rcvRs <- lift $ connResults <$> subscribeQueues c (concat $ M.elems rcvQs)
|
||||
rcvRs <- lift $ connResults . fst <$> subscribeQueues c (concat $ M.elems rcvQs)
|
||||
ns <- asks ntfSupervisor
|
||||
tkn <- readTVarIO (ntfTkn ns)
|
||||
when (instantNotifications tkn) . void . lift . forkIO . void . runExceptT $ sendNtfCreate ns rcvRs conns
|
||||
@@ -985,7 +985,7 @@ subscribeConnections' c connIds = do
|
||||
writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ INTERNAL $ "subscribeConnections result size: " <> show actual <> ", expected " <> show expected)
|
||||
|
||||
resubscribeConnection' :: AgentClient -> ConnId -> AM ()
|
||||
resubscribeConnection' c connId = toConnResult connId =<< resubscribeConnections' c [connId]
|
||||
resubscribeConnection' c connId = toConnResult c connId =<< resubscribeConnections' c [connId]
|
||||
{-# INLINE resubscribeConnection' #-}
|
||||
|
||||
resubscribeConnections' :: AgentClient -> [ConnId] -> AM (Map ConnId (Either AgentErrorType ()))
|
||||
@@ -1317,7 +1317,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork
|
||||
let mId = unId msgId
|
||||
ri' = maybe id updateRetryInterval2 msgRetryState ri
|
||||
withRetryLock2 ri' qLock $ \riState loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
liftIO $ waitForUserNetwork c
|
||||
resp <- tryError $ case msgType of
|
||||
AM_CONN_INFO -> sendConfirmation c sq msgBody
|
||||
AM_CONN_INFO_REPLY -> sendConfirmation c sq msgBody
|
||||
@@ -1326,13 +1326,13 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork
|
||||
Left e -> do
|
||||
let err = if msgType == AM_A_MSG_ then MERR mId e else ERR e
|
||||
case e of
|
||||
SMP SMP.QUOTA -> case msgType of
|
||||
SMP _ SMP.QUOTA -> case msgType of
|
||||
AM_CONN_INFO -> connError msgId NOT_AVAILABLE
|
||||
AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE
|
||||
_ -> do
|
||||
expireTs <- addUTCTime (-quotaExceededTimeout) <$> liftIO getCurrentTime
|
||||
if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RISlow
|
||||
SMP SMP.AUTH -> case msgType of
|
||||
SMP _ SMP.AUTH -> case msgType of
|
||||
AM_CONN_INFO -> connError msgId NOT_AVAILABLE
|
||||
AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE
|
||||
AM_RATCHET_INFO -> connError msgId NOT_AVAILABLE
|
||||
@@ -1358,13 +1358,17 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork
|
||||
| temporaryOrHostError e -> do
|
||||
let msgTimeout = if msgType == AM_HELLO_ then helloTimeout else messageTimeout
|
||||
expireTs <- addUTCTime (-msgTimeout) <$> liftIO getCurrentTime
|
||||
if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RIFast
|
||||
if internalTs < expireTs
|
||||
then notifyDelMsgs msgId e expireTs
|
||||
else do
|
||||
when (serverHostError e) $ notify $ MWARN (unId msgId) e
|
||||
retrySndMsg RIFast
|
||||
| otherwise -> notifyDel msgId err
|
||||
where
|
||||
retrySndMsg riMode = do
|
||||
withStore' c $ \db -> updatePendingMsgRIState db connId msgId riState
|
||||
retrySndOp c $ loop riMode
|
||||
Right () -> do
|
||||
Right proxySrv_ -> do
|
||||
case msgType of
|
||||
AM_CONN_INFO -> setConfirmed
|
||||
AM_CONN_INFO_REPLY -> setConfirmed
|
||||
@@ -1385,8 +1389,8 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork
|
||||
-- With `status == Active` condition, CON is sent here only by the accepting party, that previously received HELLO
|
||||
when (status == Active) $ notify $ CON pqEncryption
|
||||
-- this branch should never be reached as receive queue is created before the confirmation,
|
||||
_ -> logError "HELLO sent without receive queue"
|
||||
AM_A_MSG_ -> notify $ SENT mId
|
||||
_ -> logError c "HELLO sent without receive queue"
|
||||
AM_A_MSG_ -> notify $ SENT mId proxySrv_
|
||||
AM_A_RCVD_ -> pure ()
|
||||
AM_QCONT_ -> pure ()
|
||||
AM_QADD_ -> pure ()
|
||||
@@ -1508,10 +1512,10 @@ switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs s
|
||||
-- try to get the server that is different from all queues, or at least from the primary rcv queue
|
||||
srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs)
|
||||
srv' <- if srv == server then getNextServer c userId [server] else pure srvAuth
|
||||
(q, qUri) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe
|
||||
(q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe
|
||||
let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId}
|
||||
rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq'
|
||||
liftIO $ addSubscription c rq''
|
||||
lift $ addNewQueueSubscription c rq'' tSess sessId
|
||||
void . enqueueMessages c cData sqs SMP.noMsgFlags $ QADD [(qUri, Just (server, sndId))]
|
||||
rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSendingQADD
|
||||
let rqs' = updatedQs rq1 rqs <> [rq'']
|
||||
@@ -1565,7 +1569,7 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni
|
||||
ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM ()
|
||||
ackQueueMessage c rq srvMsgId =
|
||||
sendAck c rq srvMsgId `catchAgentError` \case
|
||||
SMP SMP.NO_MSG -> pure ()
|
||||
SMP _ SMP.NO_MSG -> pure ()
|
||||
e -> throwError e
|
||||
|
||||
-- | Suspend SMP agent connection (OFF command) in Reader monad
|
||||
@@ -1583,7 +1587,7 @@ suspendConnection' c connId = withConnLock c connId "suspendConnection" $ do
|
||||
-- unlike deleteConnectionAsync, this function does not mark connection as deleted in case of deletion failure
|
||||
-- currently it is used only in tests
|
||||
deleteConnection' :: AgentClient -> ConnId -> AM ()
|
||||
deleteConnection' c connId = toConnResult connId =<< deleteConnections' c [connId]
|
||||
deleteConnection' c connId = toConnResult c connId =<< deleteConnections' c [connId]
|
||||
{-# INLINE deleteConnection' #-}
|
||||
|
||||
connRcvQueues :: Connection d -> [RcvQueue]
|
||||
@@ -1895,7 +1899,7 @@ deleteToken_ c tkn@NtfToken {ntfTokenId, ntfTknStatus} = do
|
||||
withStore' c $ \db -> updateNtfToken db tkn ntfTknStatus ntfTknAction
|
||||
atomically $ nsUpdateToken ns tkn {ntfTknStatus, ntfTknAction}
|
||||
agentNtfDeleteToken c tknId tkn `catchAgentError` \case
|
||||
NTF AUTH -> pure ()
|
||||
NTF _ AUTH -> pure ()
|
||||
e -> throwError e
|
||||
withStore' c $ \db -> removeNtfToken db tkn
|
||||
atomically $ nsRemoveNtfToken ns
|
||||
@@ -1912,7 +1916,7 @@ withToken c tkn@NtfToken {deviceToken, ntfMode} from_ (toStatus, toAction_) f =
|
||||
let updatedToken = tkn {ntfTknStatus = toStatus, ntfTknAction = toAction_}
|
||||
atomically $ nsUpdateToken ns updatedToken
|
||||
pure toStatus
|
||||
Left e@(NTF AUTH) -> do
|
||||
Left e@(NTF _ AUTH) -> do
|
||||
withStore' c $ \db -> removeNtfToken db tkn
|
||||
atomically $ nsRemoveNtfToken ns
|
||||
void $ registerNtfToken' c deviceToken ntfMode
|
||||
@@ -1998,9 +2002,7 @@ subscriber :: AgentClient -> AM' ()
|
||||
subscriber c@AgentClient {msgQ} = forever $ do
|
||||
t <- atomically $ readTBQueue msgQ
|
||||
agentOperationBracket c AORcvNetwork waitUntilActive $
|
||||
runExceptT (processSMPTransmission c t) >>= \case
|
||||
Left e -> liftIO $ print e
|
||||
Right _ -> return ()
|
||||
processSMPTransmissions c t
|
||||
|
||||
cleanupManager :: AgentClient -> AM' ()
|
||||
cleanupManager c@AgentClient {subQ} = do
|
||||
@@ -2074,19 +2076,64 @@ cleanupManager c@AgentClient {subQ} = do
|
||||
|
||||
data ACKd = ACKd | ACKPending
|
||||
|
||||
-- | make sure to ACK or throw in each message processing branch
|
||||
-- it cannot be finally, unfortunately, as sometimes it needs to be ACK+DEL
|
||||
processSMPTransmission :: AgentClient -> ServerTransmission SMPVersion BrokerMsg -> AM ()
|
||||
processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, sessId, isResponse, rId, cmd) = do
|
||||
(rq, SomeConn _ conn) <- withStore c (\db -> getRcvConn db srv rId)
|
||||
processSMP rq conn $ toConnData conn
|
||||
-- | Make sure to ACK or throw in each message processing branch
|
||||
-- It cannot be finally, as sometimes it needs to be ACK+DEL,
|
||||
-- and sometimes ACK has to be sent from the consumer.
|
||||
processSMPTransmissions :: AgentClient -> ServerTransmissionBatch SMPVersion ErrorType BrokerMsg -> AM' ()
|
||||
processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) = do
|
||||
upConnIds <- newTVarIO []
|
||||
forM_ ts $ \(entId, t) -> case t of
|
||||
STEvent msgOrErr ->
|
||||
withRcvConn entId $ \rq@RcvQueue {connId} conn -> case msgOrErr of
|
||||
Right msg -> processSMP rq conn (toConnData conn) msg
|
||||
Left e -> lift $ notifyErr connId e
|
||||
STResponse (Cmd SRecipient cmd) respOrErr ->
|
||||
withRcvConn entId $ \rq conn -> case cmd of
|
||||
-- TODO process expired responses to ACK and DEL
|
||||
SMP.SUB -> case respOrErr of
|
||||
Right SMP.OK -> processSubOk rq upConnIds
|
||||
Right msg@SMP.MSG {} -> do
|
||||
processSubOk rq upConnIds -- the connection is UP even when processing this particular message fails
|
||||
processSMP rq conn (toConnData conn) msg
|
||||
Right r -> processSubErr rq $ unexpectedResponse r
|
||||
Left e -> unless (temporaryClientError e) $ processSubErr rq e -- timeout/network was already reported
|
||||
_ -> pure ()
|
||||
STResponse {} -> pure () -- TODO process expired responses to sent messages
|
||||
STUnexpectedError e -> do
|
||||
logServer "<--" c srv entId $ "error: " <> bshow e
|
||||
notifyErr "" e
|
||||
connIds <- readTVarIO upConnIds
|
||||
unless (null connIds) $ notify' "" $ UP srv connIds
|
||||
where
|
||||
processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> AM ()
|
||||
withRcvConn :: SMP.RecipientId -> (forall c. RcvQueue -> Connection c -> AM ()) -> AM' ()
|
||||
withRcvConn rId a = do
|
||||
tryAgentError' (withStore c $ \db -> getRcvConn db srv rId) >>= \case
|
||||
Left e -> notify' "" (ERR e)
|
||||
Right (rq@RcvQueue {connId}, SomeConn _ conn) ->
|
||||
tryAgentError' (a rq conn) >>= \case
|
||||
Left e -> notify' connId (ERR e)
|
||||
Right () -> pure ()
|
||||
processSubOk :: RcvQueue -> TVar [ConnId] -> AM ()
|
||||
processSubOk rq@RcvQueue {connId} upConnIds =
|
||||
atomically . whenM (isPendingSub connId) $ do
|
||||
addSubscription c rq
|
||||
modifyTVar' upConnIds (connId :)
|
||||
processSubErr :: RcvQueue -> SMPClientError -> AM ()
|
||||
processSubErr rq@RcvQueue {connId} e = do
|
||||
atomically . whenM (isPendingSub connId) $ failSubscription c rq e
|
||||
lift $ notifyErr connId e
|
||||
isPendingSub connId = (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId
|
||||
notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> ACommand 'Agent e -> m ()
|
||||
notify' connId msg = atomically $ writeTBQueue subQ ("", connId, APC (sAEntity @e) msg)
|
||||
notifyErr :: ConnId -> SMPClientError -> AM' ()
|
||||
notifyErr connId = notify' connId . ERR . protocolClientError SMP (B.unpack $ strEncode srv)
|
||||
processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM ()
|
||||
processSMP
|
||||
rq@RcvQueue {e2ePrivKey, e2eDhSecret, status}
|
||||
rq@RcvQueue {rcvId = rId, e2ePrivKey, e2eDhSecret, status}
|
||||
conn
|
||||
cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} =
|
||||
withConnLock c connId "processSMP" $ case cmd of
|
||||
cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss}
|
||||
smpMsg =
|
||||
withConnLock c connId "processSMP" $ case smpMsg of
|
||||
SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} ->
|
||||
void . handleNotifyAck $ do
|
||||
msg' <- decryptSMPMessage rq msg
|
||||
@@ -2136,6 +2183,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v,
|
||||
_ -> pure ()
|
||||
let encryptedMsgHash = C.sha256Hash encAgentMessage
|
||||
g <- asks random
|
||||
atomically updateTotalMsgCount
|
||||
tryError (agentClientMsg g encryptedMsgHash) >>= \case
|
||||
Right (Just (msgId, msgMeta, aMessage, rcPrev)) -> do
|
||||
conn'' <- resetRatchetSync
|
||||
@@ -2169,6 +2217,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v,
|
||||
| otherwise = pure conn'
|
||||
Right _ -> prohibited >> ack
|
||||
Left e@(AGENT A_DUPLICATE) -> do
|
||||
atomically updateDupMsgCount
|
||||
withStore' c (\db -> getLastMsg db connId srvMsgId) >>= \case
|
||||
Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck}
|
||||
| userAck -> ackDel internalId
|
||||
@@ -2199,6 +2248,20 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v,
|
||||
checkDuplicateHash e encryptedMsgHash =
|
||||
unlessM (withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash) $
|
||||
throwError e
|
||||
updateTotalMsgCount :: STM ()
|
||||
updateTotalMsgCount =
|
||||
TM.lookup connId (msgCounts c) >>= \case
|
||||
Just v -> modifyTVar' v $ first (+ 1)
|
||||
Nothing -> addMsgCount 0
|
||||
updateDupMsgCount :: STM ()
|
||||
updateDupMsgCount =
|
||||
TM.lookup connId (msgCounts c) >>= \case
|
||||
Just v -> modifyTVar' v $ second (+ 1)
|
||||
Nothing -> addMsgCount 1
|
||||
addMsgCount :: Int -> STM ()
|
||||
addMsgCount duplicate = do
|
||||
counts <- newTVar (1, duplicate)
|
||||
TM.insert connId counts (msgCounts c)
|
||||
agentClientMsg :: TVar ChaChaDRG -> ByteString -> AM (Maybe (InternalId, MsgMeta, AMessage, CR.RatchetX448))
|
||||
agentClientMsg g encryptedMsgHash = withStore c $ \db -> runExceptT $ do
|
||||
rc <- ExceptT $ getRatchet db connId -- ratchet state pre-decryption - required for processing EREADY
|
||||
@@ -2238,27 +2301,22 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v,
|
||||
handleNotifyAck :: AM ACKd -> AM ACKd
|
||||
handleNotifyAck m = m `catchAgentError` \e -> notify (ERR e) >> ack
|
||||
SMP.END ->
|
||||
atomically (TM.lookup tSess smpClients $>>= (tryReadTMVar . sessionVar) >>= processEND)
|
||||
>>= logServer "<--" c srv rId
|
||||
atomically (TM.lookup tSess (smpClients c) $>>= (tryReadTMVar . sessionVar) >>= processEND)
|
||||
>>= notifyEnd
|
||||
where
|
||||
processEND = \case
|
||||
Just (Right clnt)
|
||||
| sessId == sessionId (thParams clnt) -> do
|
||||
removeSubscription c connId
|
||||
notify' END
|
||||
pure "END"
|
||||
| otherwise -> ignored
|
||||
_ -> ignored
|
||||
ignored = pure "END from disconnected client - ignored"
|
||||
_ -> do
|
||||
logServer "<--" c srv rId $ "unexpected: " <> bshow cmd
|
||||
notify . ERR $ BROKER (B.unpack $ strEncode srv) $ if isResponse then TIMEOUT else UNEXPECTED
|
||||
| sessId == sessionId (thParams $ connectedClient clnt) ->
|
||||
removeSubscription c connId $> True
|
||||
_ -> pure False
|
||||
notifyEnd removed
|
||||
| removed = notify END >> logServer "<--" c srv rId "END"
|
||||
| otherwise = logServer "<--" c srv rId "END from disconnected client - ignored"
|
||||
SMP.ERR e -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e
|
||||
r -> unexpected r
|
||||
where
|
||||
notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m ()
|
||||
notify = atomically . notify'
|
||||
|
||||
notify' :: forall e. AEntityI e => ACommand 'Agent e -> STM ()
|
||||
notify' msg = writeTBQueue subQ ("", connId, APC (sAEntity @e) msg)
|
||||
notify :: forall e m. (AEntityI e, MonadIO m) => ACommand 'Agent e -> m ()
|
||||
notify = notify' connId
|
||||
|
||||
prohibited :: AM ()
|
||||
prohibited = notify . ERR $ AGENT A_PROHIBITED
|
||||
@@ -2266,6 +2324,12 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v,
|
||||
enqueueCmd :: InternalCommand -> AM ()
|
||||
enqueueCmd = enqueueCommand c "" connId (Just srv) . AInternalCommand
|
||||
|
||||
unexpected :: BrokerMsg -> AM ()
|
||||
unexpected r = do
|
||||
logServer "<--" c srv rId $ "unexpected: " <> bshow r
|
||||
-- TODO add extended information about transmission type once UNEXPECTED has string
|
||||
notify . ERR $ BROKER (B.unpack $ strEncode srv) $ UNEXPECTED (take 32 $ show r)
|
||||
|
||||
decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> AM (SMP.PrivHeader, AgentMsgEnvelope)
|
||||
decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do
|
||||
clientMsg <- agentCbDecrypt e2eDh cmNonce cmEncBody
|
||||
@@ -2602,7 +2666,7 @@ confirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do
|
||||
confirmQueue :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM ()
|
||||
confirmQueue c cData@ConnData {connId, connAgentVersion, pqSupport} sq srv connInfo e2eEncryption_ subMode = do
|
||||
msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode
|
||||
sendConfirmation c sq msg
|
||||
void $ sendConfirmation c sq msg
|
||||
withStore' c $ \db -> setSndQueueStatus db sq Confirmed
|
||||
where
|
||||
mkConfirmation :: AgentMessage -> AM MsgBody
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,8 @@ data AgentConfig = AgentConfig
|
||||
xftpCfg :: XFTPClientConfig,
|
||||
reconnectInterval :: RetryInterval,
|
||||
messageRetryInterval :: RetryInterval2,
|
||||
userNetworkInterval :: RetryInterval,
|
||||
userNetworkInterval :: Int,
|
||||
userOfflineDelay :: NominalDiffTime,
|
||||
messageTimeout :: NominalDiffTime,
|
||||
connDeleteDeliveryTimeout :: NominalDiffTime,
|
||||
helloTimeout :: NominalDiffTime,
|
||||
@@ -147,14 +148,6 @@ defaultMessageRetryInterval =
|
||||
}
|
||||
}
|
||||
|
||||
defaultUserNetworkInterval :: RetryInterval
|
||||
defaultUserNetworkInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 1200_000000, -- 20 minutes
|
||||
increaseAfter = 0,
|
||||
maxInterval = 7200_000000 -- 2 hours
|
||||
}
|
||||
|
||||
defaultAgentConfig :: AgentConfig
|
||||
defaultAgentConfig =
|
||||
AgentConfig
|
||||
@@ -170,7 +163,8 @@ defaultAgentConfig =
|
||||
xftpCfg = defaultXFTPClientConfig,
|
||||
reconnectInterval = defaultReconnectInterval,
|
||||
messageRetryInterval = defaultMessageRetryInterval,
|
||||
userNetworkInterval = defaultUserNetworkInterval,
|
||||
userNetworkInterval = 1800_000000, -- 30 minutes, should be less than Int32 max value
|
||||
userOfflineDelay = 2, -- if network offline event happens in less than 2 seconds after it was set online, it is ignored
|
||||
messageTimeout = 2 * nominalDay,
|
||||
connDeleteDeliveryTimeout = 2 * nominalDay,
|
||||
helloTimeout = 2 * nominalDay,
|
||||
@@ -179,7 +173,7 @@ defaultAgentConfig =
|
||||
cleanupInterval = 30 * 60 * 1000000, -- 30 minutes
|
||||
cleanupStepInterval = 200000, -- 200ms
|
||||
maxWorkerRestartsPerMin = 5,
|
||||
-- 3 consecutive subscription timeouts will result in alert to the user
|
||||
-- 5 consecutive subscription timeouts will result in alert to the user
|
||||
-- this is a fallback, as the timeout set to 3x of expected timeout, to avoid potential locking.
|
||||
maxSubscriptionTimeouts = 5,
|
||||
storedMsgDataTTL = 21 * nominalDay,
|
||||
|
||||
@@ -18,7 +18,6 @@ module Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple (logError, logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
@@ -56,20 +55,20 @@ runNtfSupervisor c = do
|
||||
where
|
||||
handleErr :: ConnId -> AM' () -> AM' ()
|
||||
handleErr connId = E.handle $ \(e :: E.SomeException) -> do
|
||||
logError $ "runNtfSupervisor error " <> tshow e
|
||||
logError c $ "runNtfSupervisor error " <> tshow e
|
||||
notifyErr connId e
|
||||
notifyErr connId e = notifyInternalError c connId $ "runNtfSupervisor error " <> show e
|
||||
|
||||
processNtfSub :: AgentClient -> (ConnId, NtfSupervisorCommand) -> AM ()
|
||||
processNtfSub c (connId, cmd) = do
|
||||
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
|
||||
logInfo c $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
|
||||
case cmd of
|
||||
NSCCreate -> do
|
||||
(a, RcvQueue {server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
|
||||
a <- liftIO $ getNtfSubscription db connId
|
||||
q <- ExceptT $ getPrimaryRcvQueue db connId
|
||||
pure (a, q)
|
||||
logInfo $ "processNtfSub, NSCCreate - a = " <> tshow a
|
||||
logInfo c $ "processNtfSub, NSCCreate - a = " <> tshow a
|
||||
case a of
|
||||
Nothing -> do
|
||||
withTokenServer $ \ntfServer -> do
|
||||
@@ -119,14 +118,14 @@ processNtfSub c (connId, cmd) = do
|
||||
sub_ <- withStore' c $ \db -> do
|
||||
supervisorUpdateNtfAction db connId (NtfSubNTFAction NSADelete)
|
||||
getNtfSubscription db connId
|
||||
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
|
||||
logInfo c $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
|
||||
case sub_ of
|
||||
(Just (NtfSubscription {ntfServer}, _)) -> lift . void $ getNtfNTFWorker True c ntfServer
|
||||
_ -> pure () -- err "NSCDelete - no subscription"
|
||||
NSCSmpDelete -> do
|
||||
withStore' c (`getPrimaryRcvQueue` connId) >>= \case
|
||||
Right rq@RcvQueue {server = smpServer} -> do
|
||||
logInfo $ "processNtfSub, NSCSmpDelete - rq = " <> tshow rq
|
||||
logInfo c $ "processNtfSub, NSCSmpDelete - rq = " <> tshow rq
|
||||
withStore' c $ \db -> supervisorUpdateNtfAction db connId (NtfSubSMPAction NSASmpDelete)
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
|
||||
@@ -158,10 +157,10 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
runNtfOperation =
|
||||
withWork c doWork (`getNextNtfSubNTFAction` srv) $
|
||||
\nextSub@(NtfSubscription {connId}, _, _) -> do
|
||||
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
|
||||
logInfo c $ "runNtfWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
|
||||
@@ -242,10 +241,10 @@ runNtfSMPWorker c srv Worker {doWork} = do
|
||||
runNtfSMPOperation =
|
||||
withWork c doWork (`getNextNtfSubSMPAction` srv) $
|
||||
\nextSub@(NtfSubscription {connId}, _, _) -> do
|
||||
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
|
||||
logInfo c $ "runNtfSMPWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM ()
|
||||
@@ -288,7 +287,7 @@ rescheduleAction doWork ts actionTs
|
||||
|
||||
retryOnError :: AgentClient -> Text -> AM () -> (AgentErrorType -> AM ()) -> AgentErrorType -> AM ()
|
||||
retryOnError c name loop done e = do
|
||||
logError $ name <> " error: " <> tshow e
|
||||
logWarn c $ name <> " error: " <> tshow e
|
||||
case e of
|
||||
BROKER _ NETWORK -> retryLoop
|
||||
BROKER _ TIMEOUT -> retryLoop
|
||||
|
||||
@@ -114,6 +114,7 @@ module Simplex.Messaging.Agent.Protocol
|
||||
BrokerErrorType (..),
|
||||
SMPAgentError (..),
|
||||
AgentCryptoError (..),
|
||||
AgentLogLevel (..),
|
||||
cryptoErrToSyncState,
|
||||
ATransmission,
|
||||
ATransmissionOrError,
|
||||
@@ -156,6 +157,7 @@ module Simplex.Messaging.Agent.Protocol
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Logger.Simple (LogLevel (..))
|
||||
import Control.Monad (unless)
|
||||
import Control.Monad.Except (runExceptT, throwError)
|
||||
import Control.Monad.IO.Class
|
||||
@@ -189,23 +191,25 @@ import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType)
|
||||
import Simplex.Messaging.Agent.QueryString
|
||||
import Simplex.Messaging.Client (ProxyClientError)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet
|
||||
( InitialKeys (..),
|
||||
PQEncryption (..),
|
||||
pattern PQEncOff,
|
||||
PQSupport,
|
||||
pattern PQSupportOn,
|
||||
pattern PQSupportOff,
|
||||
RcvE2ERatchetParams,
|
||||
RcvE2ERatchetParamsUri,
|
||||
SndE2ERatchetParams
|
||||
SndE2ERatchetParams,
|
||||
pattern PQEncOff,
|
||||
pattern PQSupportOff,
|
||||
pattern PQSupportOn,
|
||||
)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol
|
||||
( AProtocolType,
|
||||
BrokerErrorType (..),
|
||||
EntityId,
|
||||
ErrorType,
|
||||
MsgBody,
|
||||
@@ -213,14 +217,14 @@ import Simplex.Messaging.Protocol
|
||||
MsgId,
|
||||
NMsgMeta,
|
||||
ProtocolServer (..),
|
||||
SMPClientVersion,
|
||||
SMPMsgMeta,
|
||||
SMPServer,
|
||||
SMPServerWithAuth,
|
||||
SndPublicAuthKey,
|
||||
SubscriptionMode,
|
||||
SMPClientVersion,
|
||||
VersionSMPC,
|
||||
VersionRangeSMPC,
|
||||
VersionSMPC,
|
||||
initialSMPClientVersion,
|
||||
legacyEncodeServer,
|
||||
legacyServerP,
|
||||
@@ -233,7 +237,7 @@ import Simplex.Messaging.Protocol
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme
|
||||
import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP)
|
||||
import Simplex.Messaging.Transport (Transport (..))
|
||||
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..))
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
@@ -389,11 +393,13 @@ data ACommand (p :: AParty) (e :: AEntity) where
|
||||
DISCONNECT :: AProtocolType -> TransportHost -> ACommand Agent AENone
|
||||
DOWN :: SMPServer -> [ConnId] -> ACommand Agent AENone
|
||||
UP :: SMPServer -> [ConnId] -> ACommand Agent AENone
|
||||
LOG :: AgentLogLevel -> Text -> ACommand Agent AENone
|
||||
SWITCH :: QueueDirection -> SwitchPhase -> ConnectionStats -> ACommand Agent AEConn
|
||||
RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> ACommand Agent AEConn
|
||||
SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn
|
||||
MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn
|
||||
SENT :: AgentMsgId -> ACommand Agent AEConn
|
||||
SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn
|
||||
MWARN :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn
|
||||
MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn
|
||||
MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn
|
||||
MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn
|
||||
@@ -452,11 +458,13 @@ data ACommandTag (p :: AParty) (e :: AEntity) where
|
||||
DISCONNECT_ :: ACommandTag Agent AENone
|
||||
DOWN_ :: ACommandTag Agent AENone
|
||||
UP_ :: ACommandTag Agent AENone
|
||||
LOG_ :: ACommandTag Agent AENone
|
||||
SWITCH_ :: ACommandTag Agent AEConn
|
||||
RSYNC_ :: ACommandTag Agent AEConn
|
||||
SEND_ :: ACommandTag Client AEConn
|
||||
MID_ :: ACommandTag Agent AEConn
|
||||
SENT_ :: ACommandTag Agent AEConn
|
||||
MWARN_ :: ACommandTag Agent AEConn
|
||||
MERR_ :: ACommandTag Agent AEConn
|
||||
MERRS_ :: ACommandTag Agent AEConn
|
||||
MSG_ :: ACommandTag Agent AEConn
|
||||
@@ -508,11 +516,13 @@ aCommandTag = \case
|
||||
DISCONNECT {} -> DISCONNECT_
|
||||
DOWN {} -> DOWN_
|
||||
UP {} -> UP_
|
||||
LOG {} -> LOG_
|
||||
SWITCH {} -> SWITCH_
|
||||
RSYNC {} -> RSYNC_
|
||||
SEND {} -> SEND_
|
||||
MID {} -> MID_
|
||||
SENT _ -> SENT_
|
||||
SENT {} -> SENT_
|
||||
MWARN {} -> MWARN_
|
||||
MERR {} -> MERR_
|
||||
MERRS {} -> MERRS_
|
||||
MSG {} -> MSG_
|
||||
@@ -908,7 +918,7 @@ instance Encoding AgentMsgEnvelope where
|
||||
-- AgentRatchetInfo is not encrypted with double ratchet, but with per-queue E2E encryption
|
||||
data AgentMessage
|
||||
= -- used by the initiating party when confirming reply queue
|
||||
AgentConnInfo ConnInfo
|
||||
AgentConnInfo ConnInfo
|
||||
| -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation.
|
||||
-- It made removed REPLY message unnecessary.
|
||||
AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo
|
||||
@@ -1300,6 +1310,7 @@ instance VersionRangeI SMPClientVersion SMPQueueUri where
|
||||
type VersionT SMPClientVersion SMPQueueUri = SMPQueueInfo
|
||||
versionRange = clientVRange
|
||||
toVersionT (SMPQueueUri _vr addr) v = SMPQueueInfo v addr
|
||||
toVersionRange (SMPQueueUri _vr addr) vr = SMPQueueUri vr addr
|
||||
|
||||
-- | SMP queue information sent out-of-band.
|
||||
--
|
||||
@@ -1382,9 +1393,9 @@ deriving instance Show (ConnectionRequestUri m)
|
||||
data AConnectionRequestUri = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequestUri m)
|
||||
|
||||
instance Eq AConnectionRequestUri where
|
||||
ACR m cr == ACR m' cr' = case testEquality m m' of
|
||||
Just Refl -> cr == cr'
|
||||
_ -> False
|
||||
ACR m cr == ACR m' cr' = case testEquality m m' of
|
||||
Just Refl -> cr == cr'
|
||||
_ -> False
|
||||
|
||||
deriving instance Show AConnectionRequestUri
|
||||
|
||||
@@ -1469,11 +1480,13 @@ data AgentErrorType
|
||||
| -- | connection errors
|
||||
CONN {connErr :: ConnectionErrorType}
|
||||
| -- | SMP protocol errors forwarded to agent clients
|
||||
SMP {smpErr :: ErrorType}
|
||||
SMP {serverAddress :: String, smpErr :: ErrorType}
|
||||
| -- | NTF protocol errors forwarded to agent clients
|
||||
NTF {ntfErr :: ErrorType}
|
||||
NTF {serverAddress :: String, ntfErr :: ErrorType}
|
||||
| -- | XFTP protocol errors forwarded to agent clients
|
||||
XFTP {xftpErr :: XFTPErrorType}
|
||||
XFTP {serverAddress :: String, xftpErr :: XFTPErrorType}
|
||||
| -- | SMP proxy errors
|
||||
PROXY {proxyServer :: String, relayServer :: String, proxyErr :: ProxyClientError}
|
||||
| -- | XRCP protocol errors forwarded to agent clients
|
||||
RCP {rcpErr :: RCErrorType}
|
||||
| -- | SMP server errors
|
||||
@@ -1516,22 +1529,6 @@ data ConnectionErrorType
|
||||
NOT_AVAILABLE
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | SMP server errors.
|
||||
data BrokerErrorType
|
||||
= -- | invalid server response (failed to parse)
|
||||
RESPONSE {smpErr :: String}
|
||||
| -- | unexpected response
|
||||
UNEXPECTED
|
||||
| -- | network error
|
||||
NETWORK
|
||||
| -- | no compatible server host (e.g. onion when public is required, or vice versa)
|
||||
HOST
|
||||
| -- | handshake or other transport error
|
||||
TRANSPORT {transportErr :: TransportError}
|
||||
| -- | command response timeout
|
||||
TIMEOUT
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | Errors of another SMP agent.
|
||||
data SMPAgentError
|
||||
= -- | client or agent message that failed to parse
|
||||
@@ -1566,12 +1563,14 @@ data AgentCryptoError
|
||||
|
||||
instance StrEncoding AgentCryptoError where
|
||||
strP =
|
||||
"DECRYPT_AES" $> DECRYPT_AES
|
||||
<|> "DECRYPT_CB" $> DECRYPT_CB
|
||||
<|> "RATCHET_HEADER" $> RATCHET_HEADER
|
||||
<|> "RATCHET_EARLIER " *> (RATCHET_EARLIER <$> strP)
|
||||
<|> "RATCHET_SKIPPED " *> (RATCHET_SKIPPED <$> strP)
|
||||
<|> "RATCHET_SYNC" $> RATCHET_SYNC
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"DECRYPT_AES" -> pure DECRYPT_AES
|
||||
"DECRYPT_CB" -> pure DECRYPT_CB
|
||||
"RATCHET_HEADER" -> pure RATCHET_HEADER
|
||||
"RATCHET_EARLIER" -> RATCHET_EARLIER <$> _strP
|
||||
"RATCHET_SKIPPED" -> RATCHET_SKIPPED <$> _strP
|
||||
"RATCHET_SYNC" -> pure RATCHET_SYNC
|
||||
_ -> fail "AgentCryptoError"
|
||||
strEncode = \case
|
||||
DECRYPT_AES -> "DECRYPT_AES"
|
||||
DECRYPT_CB -> "DECRYPT_CB"
|
||||
@@ -1582,42 +1581,88 @@ instance StrEncoding AgentCryptoError where
|
||||
|
||||
instance StrEncoding AgentErrorType where
|
||||
strP =
|
||||
"CMD " *> (CMD <$> parseRead1)
|
||||
<|> "CONN " *> (CONN <$> parseRead1)
|
||||
<|> "SMP " *> (SMP <$> strP)
|
||||
<|> "NTF " *> (NTF <$> strP)
|
||||
<|> "XFTP " *> (XFTP <$> strP)
|
||||
<|> "RCP " *> (RCP <$> strP)
|
||||
<|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP))
|
||||
<|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP))
|
||||
<|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1)
|
||||
<|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString)
|
||||
<|> "AGENT QUEUE " *> (AGENT . A_QUEUE <$> parseRead A.takeByteString)
|
||||
<|> "AGENT " *> (AGENT <$> parseRead1)
|
||||
<|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString)
|
||||
<|> "CRITICAL " *> (CRITICAL <$> parseRead1 <* A.space <*> parseRead A.takeByteString)
|
||||
<|> "INACTIVE" $> INACTIVE
|
||||
A.takeTill (== ' ')
|
||||
>>= \case
|
||||
"CMD" -> CMD <$> (A.space *> parseRead1)
|
||||
"CONN" -> CONN <$> (A.space *> parseRead1)
|
||||
"SMP" -> SMP <$> (A.space *> srvP) <*> _strP
|
||||
"NTF" -> NTF <$> (A.space *> srvP) <*> _strP
|
||||
"XFTP" -> XFTP <$> (A.space *> srvP) <*> _strP
|
||||
"PROXY" -> PROXY <$> (A.space *> srvP) <* A.space <*> srvP <*> _strP
|
||||
"RCP" -> RCP <$> _strP
|
||||
"BROKER" -> BROKER <$> (A.space *> srvP) <*> _strP
|
||||
"AGENT" -> AGENT <$> _strP
|
||||
"INTERNAL" -> INTERNAL <$> (A.space *> textP)
|
||||
"CRITICAL" -> CRITICAL <$> (A.space *> parseRead1) <*> (A.space *> textP)
|
||||
"INACTIVE" -> pure INACTIVE
|
||||
_ -> fail "bad AgentErrorType"
|
||||
where
|
||||
textP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ')
|
||||
srvP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ')
|
||||
textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString
|
||||
strEncode = \case
|
||||
CMD e -> "CMD " <> bshow e
|
||||
CONN e -> "CONN " <> bshow e
|
||||
SMP e -> "SMP " <> strEncode e
|
||||
NTF e -> "NTF " <> strEncode e
|
||||
XFTP e -> "XFTP " <> strEncode e
|
||||
SMP srv e -> "SMP " <> text srv <> " " <> strEncode e
|
||||
NTF srv e -> "NTF " <> text srv <> " " <> strEncode e
|
||||
XFTP srv e -> "XFTP " <> text srv <> " " <> strEncode e
|
||||
PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e]
|
||||
RCP e -> "RCP " <> strEncode e
|
||||
BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e
|
||||
BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e
|
||||
BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e
|
||||
AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e
|
||||
AGENT (A_QUEUE e) -> "AGENT QUEUE " <> bshow e
|
||||
AGENT e -> "AGENT " <> bshow e
|
||||
INTERNAL e -> "INTERNAL " <> bshow e
|
||||
CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> bshow e
|
||||
BROKER srv e -> B.unwords ["BROKER", text srv, strEncode e]
|
||||
AGENT e -> "AGENT " <> strEncode e
|
||||
INTERNAL e -> "INTERNAL " <> encodeUtf8 (T.pack e)
|
||||
CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> encodeUtf8 (T.pack e)
|
||||
INACTIVE -> "INACTIVE"
|
||||
where
|
||||
text = encodeUtf8 . T.pack
|
||||
|
||||
instance StrEncoding SMPAgentError where
|
||||
strP =
|
||||
A.takeTill (== ' ')
|
||||
>>= \case
|
||||
"MESSAGE" -> pure A_MESSAGE
|
||||
"PROHIBITED" -> pure A_PROHIBITED
|
||||
"VERSION" -> pure A_VERSION
|
||||
"CRYPTO" -> A_CRYPTO <$> _strP
|
||||
"DUPLICATE" -> pure A_DUPLICATE
|
||||
"QUEUE" -> A_QUEUE . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
_ -> fail "bad SMPAgentError"
|
||||
strEncode = \case
|
||||
A_MESSAGE -> "MESSAGE"
|
||||
A_PROHIBITED -> "PROHIBITED"
|
||||
A_VERSION -> "VERSION"
|
||||
A_CRYPTO e -> "CRYPTO " <> strEncode e
|
||||
A_DUPLICATE -> "DUPLICATE"
|
||||
A_QUEUE e -> "QUEUE " <> encodeUtf8 (T.pack e)
|
||||
|
||||
newtype AgentLogLevel = AgentLogLevel LogLevel
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding AgentLogLevel where
|
||||
strEncode (AgentLogLevel ll) = case ll of
|
||||
LogTrace -> "TRACE"
|
||||
LogDebug -> "DEBUG"
|
||||
LogInfo -> "INFO"
|
||||
LogNote -> "NOTE"
|
||||
LogWarn -> "WARN"
|
||||
LogError -> "ERROR"
|
||||
strP = do
|
||||
s <- A.takeTill (== ' ')
|
||||
AgentLogLevel <$> case s of
|
||||
"TRACE" -> pure LogTrace
|
||||
"DEBUG" -> pure LogDebug
|
||||
"INFO" -> pure LogInfo
|
||||
"NOTE" -> pure LogNote
|
||||
"WARN" -> pure LogWarn
|
||||
"ERROR" -> pure LogError
|
||||
_ -> fail "bad AgentLogLevel"
|
||||
|
||||
instance ToJSON AgentLogLevel where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromJSON AgentLogLevel where
|
||||
parseJSON = strParseJSON "AgentLogLevel"
|
||||
|
||||
cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState
|
||||
cryptoErrToSyncState = \case
|
||||
DECRYPT_AES -> RSAllowed
|
||||
@@ -1655,11 +1700,13 @@ instance StrEncoding ACmdTag where
|
||||
"DISCONNECT" -> nt DISCONNECT_
|
||||
"DOWN" -> nt DOWN_
|
||||
"UP" -> nt UP_
|
||||
"LOG" -> nt LOG_
|
||||
"SWITCH" -> ct SWITCH_
|
||||
"RSYNC" -> ct RSYNC_
|
||||
"SEND" -> t SEND_
|
||||
"MID" -> ct MID_
|
||||
"SENT" -> ct SENT_
|
||||
"MWARN" -> ct MWARN_
|
||||
"MERR" -> ct MERR_
|
||||
"MERRS" -> ct MERRS_
|
||||
"MSG" -> ct MSG_
|
||||
@@ -1713,11 +1760,13 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where
|
||||
DISCONNECT_ -> "DISCONNECT"
|
||||
DOWN_ -> "DOWN"
|
||||
UP_ -> "UP"
|
||||
LOG_ -> "LOG"
|
||||
SWITCH_ -> "SWITCH"
|
||||
RSYNC_ -> "RSYNC"
|
||||
SEND_ -> "SEND"
|
||||
MID_ -> "MID"
|
||||
SENT_ -> "SENT"
|
||||
MWARN_ -> "MWARN"
|
||||
MERR_ -> "MERR"
|
||||
MERRS_ -> "MERRS"
|
||||
MSG_ -> "MSG"
|
||||
@@ -1785,10 +1834,12 @@ commandP binaryP =
|
||||
DISCONNECT_ -> s (DISCONNECT <$> strP_ <*> strP)
|
||||
DOWN_ -> s (DOWN <$> strP_ <*> connections)
|
||||
UP_ -> s (UP <$> strP_ <*> connections)
|
||||
LOG_ -> s (LOG <$> strP_ <*> strP)
|
||||
SWITCH_ -> s (SWITCH <$> strP_ <*> strP_ <*> strP)
|
||||
RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP)
|
||||
MID_ -> s (MID <$> A.decimal <*> _strP)
|
||||
SENT_ -> s (SENT <$> A.decimal)
|
||||
SENT_ -> s (SENT <$> A.decimal <*> _strP)
|
||||
MWARN_ -> s (MWARN <$> A.decimal <* A.space <*> strP)
|
||||
MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP)
|
||||
MERRS_ -> s (MERRS <$> strP_ <*> strP)
|
||||
MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP)
|
||||
@@ -1847,11 +1898,13 @@ serializeCommand = \case
|
||||
DISCONNECT p h -> s (DISCONNECT_, p, h)
|
||||
DOWN srv conns -> B.unwords [s DOWN_, s srv, connections conns]
|
||||
UP srv conns -> B.unwords [s UP_, s srv, connections conns]
|
||||
LOG ll txt -> s (LOG_, ll, txt)
|
||||
SWITCH dir phase srvs -> s (SWITCH_, dir, phase, srvs)
|
||||
RSYNC rrState cryptoErr cstats -> s (RSYNC_, rrState, cryptoErr, cstats)
|
||||
SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody]
|
||||
MID mId pqEnc -> s (MID_, mId, pqEnc)
|
||||
SENT mId -> s (SENT_, mId)
|
||||
SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_)
|
||||
MWARN mId e -> s (MWARN_, mId, e)
|
||||
MERR mId e -> s (MERR_, mId, e)
|
||||
MERRS mIds e -> s (MERRS_, mIds, e)
|
||||
MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody]
|
||||
@@ -1977,8 +2030,6 @@ $(J.deriveJSON (sumTypeJSON id) ''CommandErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''ConnectionErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''SMPAgentError)
|
||||
|
||||
@@ -30,7 +30,7 @@ import Data.Type.Equality
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval (RI2State)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, PQEncryption, PQSupport)
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport, RatchetX448)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
( MsgBody,
|
||||
@@ -593,6 +593,8 @@ type AsyncCmdId = Int64
|
||||
data StoreError
|
||||
= -- | IO exceptions in store actions.
|
||||
SEInternal ByteString
|
||||
| -- | Database busy
|
||||
SEDatabaseBusy ByteString
|
||||
| -- | Failed to generate unique random ID
|
||||
SEUniqueID
|
||||
| -- | User ID not found
|
||||
|
||||
@@ -221,6 +221,7 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
@@ -268,7 +269,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations (DownMigration (..), MTRE
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys, PQEncryption (..), PQSupport (..))
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -278,7 +279,7 @@ import Simplex.Messaging.Parsers (blobFieldParser, defaultJSON, dropPrefix, from
|
||||
import Simplex.Messaging.Protocol
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, tshow, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Version.Internal
|
||||
import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist)
|
||||
import System.Exit (exitFailure)
|
||||
@@ -286,6 +287,7 @@ import System.FilePath (takeDirectory)
|
||||
import System.IO (hFlush, stdout)
|
||||
import UnliftIO.Exception (bracketOnError, onException)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.MVar
|
||||
import UnliftIO.STM
|
||||
|
||||
-- * SQLite Store implementation
|
||||
@@ -381,8 +383,8 @@ connectSQLiteStore :: FilePath -> ScrubbedBytes -> Bool -> IO SQLiteStore
|
||||
connectSQLiteStore dbFilePath key keepKey = do
|
||||
dbNew <- not <$> doesFileExist dbFilePath
|
||||
dbConn <- dbBusyLoop (connectDB dbFilePath key)
|
||||
dbConnection <- newMVar dbConn
|
||||
atomically $ do
|
||||
dbConnection <- newTMVar dbConn
|
||||
dbKey <- newTVar $! storeKey key keepKey
|
||||
dbClosed <- newTVar False
|
||||
pure SQLiteStore {dbFilePath, dbKey, dbConnection, dbNew, dbClosed}
|
||||
@@ -420,14 +422,14 @@ openSQLiteStore st@SQLiteStore {dbClosed} key keepKey =
|
||||
openSQLiteStore_ :: SQLiteStore -> ScrubbedBytes -> Bool -> IO ()
|
||||
openSQLiteStore_ SQLiteStore {dbConnection, dbFilePath, dbKey, dbClosed} key keepKey =
|
||||
bracketOnError
|
||||
(atomically $ takeTMVar dbConnection)
|
||||
(atomically . tryPutTMVar dbConnection)
|
||||
(takeMVar dbConnection)
|
||||
(tryPutMVar dbConnection)
|
||||
$ \DB.Connection {slow} -> do
|
||||
DB.Connection {conn} <- connectDB dbFilePath key
|
||||
atomically $ do
|
||||
putTMVar dbConnection DB.Connection {conn, slow}
|
||||
writeTVar dbClosed False
|
||||
writeTVar dbKey $! storeKey key keepKey
|
||||
putMVar dbConnection DB.Connection {conn, slow}
|
||||
|
||||
reopenSQLiteStore :: SQLiteStore -> IO ()
|
||||
reopenSQLiteStore st@SQLiteStore {dbKey, dbClosed} =
|
||||
@@ -1272,12 +1274,16 @@ createCommand :: DB.Connection -> ACorrId -> ConnId -> Maybe SMPServer -> AgentC
|
||||
createCommand db corrId connId srv_ cmd = runExceptT $ do
|
||||
(host_, port_, serverKeyHash_) <- serverFields
|
||||
createdAt <- liftIO getCurrentTime
|
||||
liftIO $
|
||||
liftIO . E.handle handleErr $
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO commands (host, port, corr_id, conn_id, command_tag, command, server_key_hash, created_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(host_, port_, corrId, connId, agentCommandTag cmd, cmd, serverKeyHash_, createdAt)
|
||||
(host_, port_, corrId, connId, cmdTag, cmd, serverKeyHash_, createdAt)
|
||||
where
|
||||
cmdTag = agentCommandTag cmd
|
||||
handleErr e
|
||||
| SQL.sqlError e == SQL.ErrorConstraint = logError $ "tried to create command " <> tshow cmdTag <> " for deleted connection"
|
||||
| otherwise = E.throwIO e
|
||||
serverFields :: ExceptT StoreError IO (Maybe (NonEmpty TransportHost), Maybe ServiceName, Maybe C.KeyHash)
|
||||
serverFields = case srv_ of
|
||||
Just srv@(SMPServer host port _) ->
|
||||
@@ -2269,20 +2275,20 @@ getXFTPServerId_ db ProtocolServer {host, port, keyHash} = do
|
||||
firstRow fromOnly SEXFTPServerNotFound $
|
||||
DB.query db "SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ?" (host, port, keyHash)
|
||||
|
||||
createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId)
|
||||
createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file = runExceptT $ do
|
||||
(rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing
|
||||
createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId)
|
||||
createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file approvedRelays = runExceptT $ do
|
||||
(rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing approvedRelays
|
||||
liftIO $
|
||||
forM_ chunks $ \fc@FileChunk {replicas} -> do
|
||||
chunkId <- insertRcvFileChunk db fc rcvFileId
|
||||
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId
|
||||
pure rcvFileEntityId
|
||||
|
||||
createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId)
|
||||
createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect"
|
||||
createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile = runExceptT $ do
|
||||
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing
|
||||
(_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId)
|
||||
createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId)
|
||||
createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect"
|
||||
createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile approvedRelays = runExceptT $ do
|
||||
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing approvedRelays
|
||||
(_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId) approvedRelays
|
||||
liftIO $
|
||||
forM_ redirectChunks $ \fc@FileChunk {replicas} -> do
|
||||
chunkId <- insertRcvFileChunk db fc redirectId
|
||||
@@ -2302,8 +2308,8 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire
|
||||
chunks = []
|
||||
}
|
||||
|
||||
insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> IO (Either StoreError (RcvFileId, DBRcvFileId))
|
||||
insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ = runExceptT $ do
|
||||
insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> Bool -> IO (Either StoreError (RcvFileId, DBRcvFileId))
|
||||
insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ approvedRelays = runExceptT $ do
|
||||
let (redirectDigest_, redirectSize_) = case redirect of
|
||||
Just RedirectFileInfo {digest = d, size = s} -> (Just d, Just s)
|
||||
Nothing -> (Nothing, Nothing)
|
||||
@@ -2311,8 +2317,8 @@ insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSiz
|
||||
createWithRandomId gVar $ \rcvFileEntityId ->
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_))
|
||||
"INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_, approvedRelays))
|
||||
rcvFileId <- liftIO $ insertedRowId db
|
||||
pure (rcvFileEntityId, rcvFileId)
|
||||
|
||||
@@ -2462,7 +2468,7 @@ deleteRcvFile' :: DB.Connection -> DBRcvFileId -> IO ()
|
||||
deleteRcvFile' db rcvFileId =
|
||||
DB.execute db "DELETE FROM rcv_files WHERE rcv_file_id = ?" (Only rcvFileId)
|
||||
|
||||
getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFileChunk))
|
||||
getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe (RcvFileChunk, Bool)))
|
||||
getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = do
|
||||
getWorkItem "rcv_file_download" getReplicaId getChunkData (markRcvFileFailed db . snd)
|
||||
where
|
||||
@@ -2486,7 +2492,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|
||||
LIMIT 1
|
||||
|]
|
||||
(host, port, keyHash, RFSReceiving, cutoffTs)
|
||||
getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError RcvFileChunk)
|
||||
getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError (RcvFileChunk, Bool))
|
||||
getChunkData (rcvFileChunkReplicaId, _fileId) =
|
||||
firstRow toChunk SEFileNotFound $
|
||||
DB.query
|
||||
@@ -2494,7 +2500,8 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|
||||
[sql|
|
||||
SELECT
|
||||
f.rcv_file_id, f.rcv_file_entity_id, f.user_id, c.rcv_file_chunk_id, c.chunk_no, c.chunk_size, c.digest, f.tmp_path, c.tmp_path,
|
||||
r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries
|
||||
r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries,
|
||||
f.approved_relays
|
||||
FROM rcv_file_chunk_replicas r
|
||||
JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id
|
||||
JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id
|
||||
@@ -2503,20 +2510,22 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|
||||
|]
|
||||
(Only rcvFileChunkReplicaId)
|
||||
where
|
||||
toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int)) -> RcvFileChunk
|
||||
toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries)) =
|
||||
RcvFileChunk
|
||||
{ rcvFileId,
|
||||
rcvFileEntityId,
|
||||
userId,
|
||||
rcvChunkId,
|
||||
chunkNo,
|
||||
chunkSize,
|
||||
digest,
|
||||
fileTmpPath,
|
||||
chunkTmpPath,
|
||||
replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}]
|
||||
}
|
||||
toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int) :. Only Bool) -> (RcvFileChunk, Bool)
|
||||
toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries) :. (Only approvedRelays)) =
|
||||
( RcvFileChunk
|
||||
{ rcvFileId,
|
||||
rcvFileEntityId,
|
||||
userId,
|
||||
rcvChunkId,
|
||||
chunkNo,
|
||||
chunkSize,
|
||||
digest,
|
||||
fileTmpPath,
|
||||
chunkTmpPath,
|
||||
replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}]
|
||||
},
|
||||
approvedRelays
|
||||
)
|
||||
|
||||
getNextRcvFileToDecrypt :: DB.Connection -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFile))
|
||||
getNextRcvFileToDecrypt db ttl =
|
||||
|
||||
@@ -22,8 +22,8 @@ import Database.SQLite.Simple (SQLError)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Util (diffToMilliseconds)
|
||||
import UnliftIO.Exception (bracket)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.MVar
|
||||
import UnliftIO.STM
|
||||
|
||||
storeKey :: ScrubbedBytes -> Bool -> Maybe ScrubbedBytes
|
||||
@@ -32,16 +32,13 @@ storeKey key keepKey = if keepKey || BA.null key then Just key else Nothing
|
||||
data SQLiteStore = SQLiteStore
|
||||
{ dbFilePath :: FilePath,
|
||||
dbKey :: TVar (Maybe ScrubbedBytes),
|
||||
dbConnection :: TMVar DB.Connection,
|
||||
dbConnection :: MVar DB.Connection,
|
||||
dbClosed :: TVar Bool,
|
||||
dbNew :: Bool
|
||||
}
|
||||
|
||||
withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
|
||||
withConnection SQLiteStore {dbConnection} =
|
||||
bracket
|
||||
(atomically $ takeTMVar dbConnection)
|
||||
(atomically . putTMVar dbConnection)
|
||||
withConnection SQLiteStore {dbConnection} = withMVar dbConnection
|
||||
|
||||
withConnection' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
|
||||
withConnection' st action = withConnection st $ action . DB.conn
|
||||
@@ -71,9 +68,9 @@ dbBusyLoop action = loop 500 3000000
|
||||
loop :: Int -> Int -> IO a
|
||||
loop t tLim =
|
||||
action `E.catch` \(e :: SQLError) ->
|
||||
let se = SQL.sqlError e in
|
||||
if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked)
|
||||
then do
|
||||
threadDelay t
|
||||
loop (t * 9 `div` 8) (tLim - t)
|
||||
else E.throwIO e
|
||||
let se = SQL.sqlError e
|
||||
in if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked)
|
||||
then do
|
||||
threadDelay t
|
||||
loop (t * 9 `div` 8) (tLim - t)
|
||||
else E.throwIO e
|
||||
|
||||
@@ -71,6 +71,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_deliver
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -110,7 +111,8 @@ schemaMigrations =
|
||||
("m20240121_message_delivery_indexes", m20240121_message_delivery_indexes, Just down_m20240121_message_delivery_indexes),
|
||||
("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect),
|
||||
("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery),
|
||||
("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem)
|
||||
("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem),
|
||||
("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20240417_rcv_files_approved_relays :: Query
|
||||
m20240417_rcv_files_approved_relays =
|
||||
[sql|
|
||||
ALTER TABLE rcv_files ADD COLUMN approved_relays INTEGER NOT NULL DEFAULT 0;
|
||||
|]
|
||||
|
||||
down_m20240417_rcv_files_approved_relays :: Query
|
||||
down_m20240417_rcv_files_approved_relays =
|
||||
[sql|
|
||||
ALTER TABLE rcv_files DROP COLUMN approved_relays;
|
||||
|]
|
||||
@@ -287,6 +287,7 @@ CREATE TABLE rcv_files(
|
||||
redirect_entity_id BLOB,
|
||||
redirect_size INTEGER,
|
||||
redirect_digest BLOB,
|
||||
approved_relays INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(rcv_file_entity_id)
|
||||
);
|
||||
CREATE TABLE rcv_file_chunks(
|
||||
|
||||
+363
-96
@@ -30,9 +30,11 @@ module Simplex.Messaging.Client
|
||||
TransportSession,
|
||||
ProtocolClient (thParams, sessionTs),
|
||||
SMPClient,
|
||||
ProxiedRelay (..),
|
||||
getProtocolClient,
|
||||
closeProtocolClient,
|
||||
protocolClientServer,
|
||||
protocolClientServer',
|
||||
transportHost',
|
||||
transportSession',
|
||||
|
||||
@@ -54,14 +56,23 @@ module Simplex.Messaging.Client
|
||||
suspendSMPQueue,
|
||||
deleteSMPQueue,
|
||||
deleteSMPQueues,
|
||||
connectSMPProxiedRelay,
|
||||
proxySMPMessage,
|
||||
forwardSMPMessage,
|
||||
sendProtocolCommand,
|
||||
|
||||
-- * Supporting types and client configuration
|
||||
ProtocolClientError (..),
|
||||
SMPClientError,
|
||||
ProxyClientError (..),
|
||||
unexpectedResponse,
|
||||
ProtocolClientConfig (..),
|
||||
NetworkConfig (..),
|
||||
TransportSessionMode (..),
|
||||
HostMode (..),
|
||||
SocksMode (..),
|
||||
SMPProxyMode (..),
|
||||
SMPProxyFallback (..),
|
||||
defaultClientConfig,
|
||||
defaultSMPClientConfig,
|
||||
defaultNetworkConfig,
|
||||
@@ -69,7 +80,9 @@ module Simplex.Messaging.Client
|
||||
chooseTransportHost,
|
||||
proxyUsername,
|
||||
temporaryClientError,
|
||||
ServerTransmission,
|
||||
smpProxyError,
|
||||
ServerTransmissionBatch,
|
||||
ServerTransmission (..),
|
||||
ClientCommand,
|
||||
|
||||
-- * For testing
|
||||
@@ -80,15 +93,18 @@ module Simplex.Messaging.Client
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
@@ -96,11 +112,14 @@ import Data.Int (Int64)
|
||||
import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import Network.Socket (ServiceName)
|
||||
import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
|
||||
import Simplex.Messaging.Protocol
|
||||
@@ -110,7 +129,7 @@ import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient)
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (bshow, diffToMicroseconds, raceAny_, threadDelay', whenM)
|
||||
import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM)
|
||||
import Simplex.Messaging.Version
|
||||
import System.Timeout (timeout)
|
||||
|
||||
@@ -128,16 +147,16 @@ data PClient v err msg = PClient
|
||||
{ connected :: TVar Bool,
|
||||
transportSession :: TransportSession msg,
|
||||
transportHost :: TransportHost,
|
||||
tcpConnectTimeout :: Int,
|
||||
tcpTimeout :: Int,
|
||||
rcvConcurrency :: Int,
|
||||
sendPings :: TVar Bool,
|
||||
lastReceived :: TVar UTCTime,
|
||||
timeoutErrorCount :: TVar Int,
|
||||
clientCorrId :: TVar ChaChaDRG,
|
||||
sentCommands :: TMap CorrId (Request err msg),
|
||||
sndQ :: TBQueue (TVar Bool, ByteString),
|
||||
sndQ :: TBQueue ByteString,
|
||||
rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)),
|
||||
msgQ :: Maybe (TBQueue (ServerTransmission v msg))
|
||||
msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg))
|
||||
}
|
||||
|
||||
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient
|
||||
@@ -158,6 +177,7 @@ smpClientStub g sessionId thVersion thAuth = do
|
||||
THandleParams
|
||||
{ sessionId,
|
||||
thVersion,
|
||||
thServerVRange = supportedServerSMPRelayVRange,
|
||||
thAuth,
|
||||
blockSize = smpBlockSize,
|
||||
implySessId = thVersion >= authCmdsSMPVersion,
|
||||
@@ -169,8 +189,8 @@ smpClientStub g sessionId thVersion thAuth = do
|
||||
{ connected,
|
||||
transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing),
|
||||
transportHost = "localhost",
|
||||
tcpConnectTimeout = 20_000_000,
|
||||
tcpTimeout = 15_000_000,
|
||||
rcvConcurrency = 8,
|
||||
sendPings,
|
||||
lastReceived,
|
||||
timeoutErrorCount,
|
||||
@@ -187,8 +207,14 @@ type SMPClient = ProtocolClient SMPVersion ErrorType BrokerMsg
|
||||
-- | Type for client command data
|
||||
type ClientCommand msg = (Maybe C.APrivateAuthKey, EntityId, ProtoCommand msg)
|
||||
|
||||
-- | Type synonym for transmission from some SPM server queue.
|
||||
type ServerTransmission v msg = (TransportSession msg, Version v, SessionId, Bool, EntityId, msg)
|
||||
-- | Type synonym for transmission from SPM servers.
|
||||
-- Batch response is presented as a single `ServerTransmissionBatch` tuple.
|
||||
type ServerTransmissionBatch v err msg = (TransportSession msg, Version v, SessionId, NonEmpty (EntityId, ServerTransmission err msg))
|
||||
|
||||
data ServerTransmission err msg
|
||||
= STEvent (Either (ProtocolClientError err) msg)
|
||||
| STResponse (ProtoCommand msg) (Either (ProtocolClientError err) msg)
|
||||
| STUnexpectedError (ProtocolClientError err)
|
||||
|
||||
data HostMode
|
||||
= -- | prefer (or require) onion hosts when connecting via SOCKS proxy
|
||||
@@ -199,16 +225,30 @@ data HostMode
|
||||
HMPublic
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SocksMode
|
||||
= -- | always use SOCKS proxy when enabled
|
||||
SMAlways
|
||||
| -- | use SOCKS proxy only for .onion hosts when no public host is available
|
||||
-- This mode is used in SMP proxy to minimize SOCKS proxy usage.
|
||||
SMOnion
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | network configuration for the client
|
||||
data NetworkConfig = NetworkConfig
|
||||
{ -- | use SOCKS5 proxy
|
||||
socksProxy :: Maybe SocksProxy,
|
||||
-- | when to use SOCKS proxy
|
||||
socksMode :: SocksMode,
|
||||
-- | determines critera which host is chosen from the list
|
||||
hostMode :: HostMode,
|
||||
-- | if above criteria is not met, if the below setting is True return error, otherwise use the first host
|
||||
requiredHostMode :: Bool,
|
||||
-- | transport sessions are created per user or per entity
|
||||
sessionMode :: TransportSessionMode,
|
||||
-- | SMP proxy mode
|
||||
smpProxyMode :: SMPProxyMode,
|
||||
-- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions
|
||||
smpProxyFallback :: SMPProxyFallback,
|
||||
-- | timeout for the initial client TCP/TLS connection (microseconds)
|
||||
tcpConnectTimeout :: Int,
|
||||
-- | timeout of protocol commands (microseconds)
|
||||
@@ -230,13 +270,30 @@ data NetworkConfig = NetworkConfig
|
||||
data TransportSessionMode = TSMUser | TSMEntity
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- SMP proxy mode for sending messages
|
||||
data SMPProxyMode
|
||||
= SPMAlways
|
||||
| SPMUnknown -- use with unknown relays
|
||||
| SPMUnprotected -- use with unknown relays when IP address is not protected (i.e., when neither SOCKS proxy nor .onion address is used)
|
||||
| SPMNever
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SMPProxyFallback
|
||||
= SPFAllow -- connect directly when chosen proxy or destination relay do not support proxy protocol.
|
||||
| SPFAllowProtected -- connect directly only when IP address is protected (SOCKS proxy or .onion address is used).
|
||||
| SPFProhibit -- prohibit direct connection to destination relay.
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultNetworkConfig :: NetworkConfig
|
||||
defaultNetworkConfig =
|
||||
NetworkConfig
|
||||
{ socksProxy = Nothing,
|
||||
socksMode = SMAlways,
|
||||
hostMode = HMOnionViaSocks,
|
||||
requiredHostMode = False,
|
||||
sessionMode = TSMUser,
|
||||
smpProxyMode = SPMNever,
|
||||
smpProxyFallback = SPFAllow,
|
||||
tcpConnectTimeout = defaultTcpConnectTimeout,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpTimeoutPerKb = 5_000,
|
||||
@@ -247,9 +304,14 @@ defaultNetworkConfig =
|
||||
logTLSErrors = False
|
||||
}
|
||||
|
||||
transportClientConfig :: NetworkConfig -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} =
|
||||
TransportClientConfig {socksProxy, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
|
||||
transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host =
|
||||
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
|
||||
where
|
||||
useSocksProxy SMAlways = socksProxy
|
||||
useSocksProxy SMOnion = case host of
|
||||
THOnionHost _ -> socksProxy
|
||||
_ -> Nothing
|
||||
{-# INLINE transportClientConfig #-}
|
||||
|
||||
-- | protocol client configuration.
|
||||
@@ -263,7 +325,7 @@ data ProtocolClientConfig v = ProtocolClientConfig
|
||||
clientALPN :: Maybe [ALPN],
|
||||
-- | client-server protocol version range
|
||||
serverVRange :: VersionRange v,
|
||||
-- | agree shared session secret (used in SMP proxy)
|
||||
-- | agree shared session secret (used in SMP proxy for additional encryption layer)
|
||||
agreeSecret :: Bool
|
||||
}
|
||||
|
||||
@@ -287,6 +349,8 @@ defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) suppo
|
||||
data Request err msg = Request
|
||||
{ corrId :: CorrId,
|
||||
entityId :: EntityId,
|
||||
command :: ProtoCommand msg,
|
||||
pending :: TVar Bool,
|
||||
responseVar :: TMVar (Either (ProtocolClientError err) msg)
|
||||
}
|
||||
|
||||
@@ -310,10 +374,14 @@ chooseTransportHost NetworkConfig {socksProxy, hostMode, requiredHostMode} hosts
|
||||
publicHost = find (not . isOnionHost) hosts
|
||||
|
||||
protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient v err msg -> String
|
||||
protocolClientServer = B.unpack . strEncode . snd3 . transportSession . client_
|
||||
protocolClientServer = B.unpack . strEncode . protocolClientServer'
|
||||
{-# INLINE protocolClientServer #-}
|
||||
|
||||
protocolClientServer' :: ProtocolClient v err msg -> ProtoServer msg
|
||||
protocolClientServer' = snd3 . transportSession . client_
|
||||
where
|
||||
snd3 (_, s, _) = s
|
||||
{-# INLINE protocolClientServer #-}
|
||||
{-# INLINE protocolClientServer' #-}
|
||||
|
||||
transportHost' :: ProtocolClient v err msg -> TransportHost
|
||||
transportHost' = transportHost . client_
|
||||
@@ -333,7 +401,7 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId)
|
||||
--
|
||||
-- A single queue can be used for multiple 'SMPClient' instances,
|
||||
-- as 'SMPServerTransmission' includes server information.
|
||||
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmission v msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do
|
||||
case chooseTransportHost networkConfig (host srv) of
|
||||
Right useHost ->
|
||||
@@ -341,7 +409,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
|
||||
Left e -> pure $ Left e
|
||||
where
|
||||
NetworkConfig {tcpConnectTimeout, tcpTimeout, rcvConcurrency, smpPingInterval} = networkConfig
|
||||
NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
|
||||
mkProtocolClient :: TransportHost -> UTCTime -> STM (PClient v err msg)
|
||||
mkProtocolClient transportHost ts = do
|
||||
connected <- newTVar False
|
||||
@@ -357,13 +425,13 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
{ connected,
|
||||
transportSession,
|
||||
transportHost,
|
||||
tcpConnectTimeout,
|
||||
tcpTimeout,
|
||||
sendPings,
|
||||
lastReceived,
|
||||
timeoutErrorCount,
|
||||
clientCorrId,
|
||||
sentCommands,
|
||||
rcvConcurrency,
|
||||
sndQ,
|
||||
rcvQ,
|
||||
msgQ
|
||||
@@ -372,7 +440,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
runClient (port', ATransport t) useHost c = do
|
||||
cVar <- newEmptyTMVarIO
|
||||
let tcConfig = (transportClientConfig networkConfig) {alpn = clientALPN}
|
||||
let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN}
|
||||
username = proxyUsername transportSession
|
||||
action <-
|
||||
async $
|
||||
@@ -402,11 +470,11 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
atomically $ do
|
||||
writeTVar (connected c) True
|
||||
putTMVar cVar $ Right c'
|
||||
raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0])
|
||||
raceAny_ ([send c' th, process c', receive c' th] <> [monitor c' | smpPingInterval > 0])
|
||||
`finally` disconnected c'
|
||||
|
||||
send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
|
||||
send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= \(active, s) -> whenM (readTVarIO active) (void $ tPutLog h s)
|
||||
send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= void . tPutLog h
|
||||
|
||||
receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
|
||||
receive ProtocolClient {client_ = PClient {rcvQ, lastReceived, timeoutErrorCount}} h = forever $ do
|
||||
@@ -414,8 +482,8 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
getCurrentTime >>= atomically . writeTVar lastReceived
|
||||
atomically $ writeTVar timeoutErrorCount 0
|
||||
|
||||
ping :: ProtocolClient v err msg -> IO ()
|
||||
ping c@ProtocolClient {client_ = PClient {sendPings, lastReceived, timeoutErrorCount}} = loop smpPingInterval
|
||||
monitor :: ProtocolClient v err msg -> IO ()
|
||||
monitor c@ProtocolClient {client_ = PClient {sendPings, lastReceived, timeoutErrorCount}} = loop smpPingInterval
|
||||
where
|
||||
loop :: Int64 -> IO ()
|
||||
loop delay = do
|
||||
@@ -435,31 +503,48 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
maxCnt = smpPingCount networkConfig
|
||||
|
||||
process :: ProtocolClient v err msg -> IO ()
|
||||
process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= mapM_ (processMsg c)
|
||||
process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= processMsgs c
|
||||
|
||||
processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO ()
|
||||
processMsg c@ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr))
|
||||
| isResponse =
|
||||
processMsgs :: ProtocolClient v err msg -> NonEmpty (SignedTransmission err msg) -> IO ()
|
||||
processMsgs c ts = do
|
||||
ts' <- catMaybes <$> mapM (processMsg c) (L.toList ts)
|
||||
forM_ msgQ $ \q ->
|
||||
mapM_ (atomically . writeTBQueue q . serverTransmission c) (L.nonEmpty ts')
|
||||
|
||||
processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO (Maybe (EntityId, ServerTransmission err msg))
|
||||
processMsg ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr))
|
||||
| B.null $ bs corrId = sendMsg $ STEvent clientResp
|
||||
| otherwise =
|
||||
atomically (TM.lookup corrId sentCommands) >>= \case
|
||||
Nothing -> sendMsg respOrErr
|
||||
Just Request {entityId, responseVar} -> atomically $ do
|
||||
TM.delete corrId sentCommands
|
||||
putTMVar responseVar $ response entityId
|
||||
| otherwise = sendMsg respOrErr
|
||||
Nothing -> sendMsg $ STUnexpectedError unexpected
|
||||
Just Request {entityId, command, pending, responseVar} -> do
|
||||
wasPending <-
|
||||
atomically $ do
|
||||
TM.delete corrId sentCommands
|
||||
ifM
|
||||
(swapTVar pending False)
|
||||
(True <$ tryPutTMVar responseVar (if entityId == entId then clientResp else Left unexpected))
|
||||
(pure False)
|
||||
if wasPending
|
||||
then pure Nothing
|
||||
else sendMsg $ if entityId == entId then STResponse command clientResp else STUnexpectedError unexpected
|
||||
where
|
||||
isResponse = not $ B.null $ bs corrId
|
||||
response entityId
|
||||
| entityId == entId =
|
||||
case respOrErr of
|
||||
Left e -> Left $ PCEResponseError e
|
||||
Right r -> case protocolError r of
|
||||
Just e -> Left $ PCEProtocolError e
|
||||
_ -> Right r
|
||||
| otherwise = Left . PCEUnexpectedResponse $ bshow respOrErr
|
||||
sendMsg :: Either err msg -> IO ()
|
||||
sendMsg = \case
|
||||
Right msg -> atomically $ mapM_ (`writeTBQueue` serverTransmission c isResponse entId msg) msgQ
|
||||
Left e -> putStrLn $ "SMP client error: " <> show e
|
||||
unexpected = unexpectedResponse respOrErr
|
||||
clientResp = case respOrErr of
|
||||
Left e -> Left $ PCEResponseError e
|
||||
Right r -> case protocolError r of
|
||||
Just e -> Left $ PCEProtocolError e
|
||||
_ -> Right r
|
||||
sendMsg :: ServerTransmission err msg -> IO (Maybe (EntityId, ServerTransmission err msg))
|
||||
sendMsg t = case msgQ of
|
||||
Just _ -> pure $ Just (entId, t)
|
||||
Nothing ->
|
||||
Nothing <$ case clientResp of
|
||||
Left e -> logError $ "SMP client error: " <> tshow e
|
||||
Right _ -> logWarn "SMP client unprocessed event"
|
||||
|
||||
unexpectedResponse :: Show r => r -> ProtocolClientError err
|
||||
unexpectedResponse = PCEUnexpectedResponse . B.pack . take 32 . show
|
||||
|
||||
proxyUsername :: TransportSession msg -> ByteString
|
||||
proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_
|
||||
@@ -510,6 +595,19 @@ temporaryClientError = \case
|
||||
_ -> False
|
||||
{-# INLINE temporaryClientError #-}
|
||||
|
||||
-- converts error of client running on proxy to the error sent to client connected to proxy
|
||||
smpProxyError :: SMPClientError -> ErrorType
|
||||
smpProxyError = \case
|
||||
PCEProtocolError e -> PROXY $ PROTOCOL e
|
||||
PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e
|
||||
PCEUnexpectedResponse e -> PROXY $ BROKER $ UNEXPECTED $ B.unpack e
|
||||
PCEResponseTimeout -> PROXY $ BROKER TIMEOUT
|
||||
PCENetworkError -> PROXY $ BROKER NETWORK
|
||||
PCEIncompatibleHost -> PROXY $ BROKER HOST
|
||||
PCETransportError t -> PROXY $ BROKER $ TRANSPORT t
|
||||
PCECryptoError _ -> CRYPTO
|
||||
PCEIOError _ -> INTERNAL
|
||||
|
||||
-- | Create a new SMP queue.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command
|
||||
@@ -523,7 +621,7 @@ createSMPQueue ::
|
||||
createSMPQueue c (rKey, rpKey) dhKey auth subMode =
|
||||
sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode) >>= \case
|
||||
IDS qik -> pure qik
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Subscribe to the SMP queue.
|
||||
--
|
||||
@@ -534,7 +632,7 @@ subscribeSMPQueue c@ProtocolClient {client_ = PClient {sendPings}} rpKey rId = d
|
||||
sendSMPCommand c (Just rpKey) rId SUB >>= \case
|
||||
OK -> pure ()
|
||||
cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Subscribe to multiple SMP queues batching commands if supported.
|
||||
subscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
|
||||
@@ -554,15 +652,15 @@ processSUBResponse :: SMPClient -> Response ErrorType BrokerMsg -> IO (Either SM
|
||||
processSUBResponse c (Response rId r) = case r of
|
||||
Right OK -> pure $ Right ()
|
||||
Right cmd@MSG {} -> writeSMPMessage c rId cmd $> Right ()
|
||||
Right r' -> pure . Left . PCEUnexpectedResponse $ bshow r'
|
||||
Right r' -> pure . Left $ unexpectedResponse r'
|
||||
Left e -> pure $ Left e
|
||||
|
||||
writeSMPMessage :: SMPClient -> RecipientId -> BrokerMsg -> IO ()
|
||||
writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c False rId msg) (msgQ $ client_ c)
|
||||
writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c [(rId, STEvent (Right msg))]) (msgQ $ client_ c)
|
||||
|
||||
serverTransmission :: ProtocolClient v err msg -> Bool -> RecipientId -> msg -> ServerTransmission v msg
|
||||
serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} isResponse entityId message =
|
||||
(transportSession, thVersion, sessionId, isResponse, entityId, message)
|
||||
serverTransmission :: ProtocolClient v err msg -> NonEmpty (RecipientId, ServerTransmission err msg) -> ServerTransmissionBatch v err msg
|
||||
serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} ts =
|
||||
(transportSession, thVersion, sessionId, ts)
|
||||
|
||||
-- | Get message from SMP queue. The server returns ERR PROHIBITED if a client uses SUB and GET via the same transport connection for the same queue
|
||||
--
|
||||
@@ -572,7 +670,7 @@ getSMPMessage c rpKey rId =
|
||||
sendSMPCommand c (Just rpKey) rId GET >>= \case
|
||||
OK -> pure Nothing
|
||||
cmd@(MSG msg) -> liftIO (writeSMPMessage c rId cmd) $> Just msg
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Subscribe to the SMP queue notifications.
|
||||
--
|
||||
@@ -600,7 +698,7 @@ enableSMPQueueNotifications :: SMPClient -> RcvPrivateAuthKey -> RecipientId ->
|
||||
enableSMPQueueNotifications c rpKey rId notifierKey rcvNtfPublicDhKey =
|
||||
sendSMPCommand c (Just rpKey) rId (NKEY notifierKey rcvNtfPublicDhKey) >>= \case
|
||||
NID nId rcvNtfSrvPublicDhKey -> pure (nId, rcvNtfSrvPublicDhKey)
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Enable notifications for the multiple queues for push notifications server.
|
||||
enableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId, NtfPublicAuthKey, RcvNtfPublicDhKey) -> IO (NonEmpty (Either SMPClientError (NotifierId, RcvNtfPublicDhKey)))
|
||||
@@ -609,7 +707,7 @@ enableSMPQueuesNtfs c qs = L.map process <$> sendProtocolCommands c cs
|
||||
cs = L.map (\(rpKey, rId, notifierKey, rcvNtfPublicDhKey) -> (Just rpKey, rId, Cmd SRecipient $ NKEY notifierKey rcvNtfPublicDhKey)) qs
|
||||
process (Response _ r) = case r of
|
||||
Right (NID nId rcvNtfSrvPublicDhKey) -> Right (nId, rcvNtfSrvPublicDhKey)
|
||||
Right r' -> Left . PCEUnexpectedResponse $ bshow r'
|
||||
Right r' -> Left $ unexpectedResponse r'
|
||||
Left e -> Left e
|
||||
|
||||
-- | Disable notifications for the queue for push notifications server.
|
||||
@@ -631,7 +729,7 @@ sendSMPMessage :: SMPClient -> Maybe SndPrivateAuthKey -> SenderId -> MsgFlags -
|
||||
sendSMPMessage c spKey sId flags msg =
|
||||
sendSMPCommand c spKey sId (SEND flags msg) >>= \case
|
||||
OK -> pure ()
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Acknowledge message delivery (server deletes the message).
|
||||
--
|
||||
@@ -641,7 +739,7 @@ ackSMPMessage c rpKey rId msgId =
|
||||
sendSMPCommand c (Just rpKey) rId (ACK msgId) >>= \case
|
||||
OK -> return ()
|
||||
cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Irreversibly suspend SMP queue.
|
||||
-- The existing messages from the queue will still be delivered.
|
||||
@@ -663,11 +761,161 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO
|
||||
deleteSMPQueues = okSMPCommands DEL
|
||||
{-# INLINE deleteSMPQueues #-}
|
||||
|
||||
-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender
|
||||
-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg
|
||||
connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay
|
||||
connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth
|
||||
| thVersion (thParams c) >= sendingProxySMPVersion =
|
||||
sendProtocolCommand_ c Nothing tOut Nothing "" (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case
|
||||
PKEY sId vr (chain, key) ->
|
||||
case supportedClientSMPRelayVRange `compatibleVersion` vr of
|
||||
Nothing -> throwE $ transportErr TEVersion
|
||||
Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key
|
||||
r -> throwE $ unexpectedResponse r
|
||||
| otherwise = throwE $ PCETransportError TEVersion
|
||||
where
|
||||
tOut = Just $ tcpConnectTimeout + tcpTimeout
|
||||
transportErr = PCEProtocolError . PROXY . BROKER . TRANSPORT
|
||||
validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519
|
||||
validateRelay (X.CertificateChain cert) exact = do
|
||||
serverKey <- case cert of
|
||||
[leaf, ca]
|
||||
| XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 ->
|
||||
C.x509ToPublic (X.certPubKey . X.signedObject $ X.getSigned leaf, []) >>= C.pubKey
|
||||
_ -> throwError "bad certificate"
|
||||
pubKey <- C.verifyX509 serverKey exact
|
||||
C.x509ToPublic (pubKey, []) >>= C.pubKey
|
||||
|
||||
data ProxiedRelay = ProxiedRelay
|
||||
{ prSessionId :: SessionId,
|
||||
prVersion :: VersionSMP,
|
||||
prServerKey :: C.PublicKeyX25519
|
||||
}
|
||||
|
||||
data ProxyClientError
|
||||
= -- | protocol error response from proxy
|
||||
ProxyProtocolError ErrorType
|
||||
| -- | unexpexted response
|
||||
ProxyUnexpectedResponse String
|
||||
| -- | error between proxy and server
|
||||
ProxyResponseError ErrorType
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
instance StrEncoding ProxyClientError where
|
||||
strEncode = \case
|
||||
ProxyProtocolError e -> "PROTOCOL " <> strEncode e
|
||||
ProxyUnexpectedResponse s -> "UNEXPECTED " <> B.pack s
|
||||
ProxyResponseError e -> "SYNTAX " <> strEncode e
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"PROTOCOL" -> ProxyProtocolError <$> _strP
|
||||
"UNEXPECTED" -> ProxyUnexpectedResponse . B.unpack <$> (A.space *> A.takeByteString)
|
||||
"SYNTAX" -> ProxyResponseError <$> _strP
|
||||
_ -> fail "bad ProxyClientError"
|
||||
|
||||
-- consider how to process slow responses - is it handled somehow locally or delegated to the caller
|
||||
-- this method is used in the client
|
||||
-- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender
|
||||
-- receives PRES :: EncResponse -> BrokerMsg -- proxy to client
|
||||
|
||||
-- When client sends message via proxy, there may be one successful scenario and 9 error scenarios
|
||||
-- as shown below (WTF stands for unexpected response, ??? for response that failed to parse).
|
||||
-- client proxy relay proxy client
|
||||
-- 0) PFWD(SEND) -> RFWD -> RRES -> PRES(OK) -> ok
|
||||
-- 1) PFWD(SEND) -> RFWD -> RRES -> PRES(ERR) -> PCEProtocolError - business logic error for client
|
||||
-- 2) PFWD(SEND) -> RFWD -> RRES -> PRES(WTF) -> PCEUnexpectedReponse - relay/client protocol logic error
|
||||
-- 3) PFWD(SEND) -> RFWD -> RRES -> PRES(???) -> PCEResponseError - relay/client syntax error
|
||||
-- 4) PFWD(SEND) -> RFWD -> ERR -> ERR PROXY PROTOCOL -> ProxyProtocolError - proxy/relay business logic error
|
||||
-- 5) PFWD(SEND) -> RFWD -> WTF -> ERR PROXY $ BROKER (UNEXPECTED s) -> ProxyProtocolError - proxy/relay protocol logic
|
||||
-- 6) PFWD(SEND) -> RFWD -> ??? -> ERR PROXY $ BROKER (RESPONSE s) -> ProxyProtocolError - - proxy/relay syntax
|
||||
-- 7) PFWD(SEND) -> ERR -> ProxyProtocolError - client/proxy business logic
|
||||
-- 8) PFWD(SEND) -> WTF -> ProxyUnexpectedResponse - client/proxy protocol logic
|
||||
-- 9) PFWD(SEND) -> ??? -> ProxyResponseError - client/proxy syntax
|
||||
--
|
||||
-- We report as proxySMPMessage error (ExceptT error) the errors of two kinds:
|
||||
-- - protocol errors from the destination relay wrapped in PRES - to simplify processing of AUTH and QUOTA errors, in this case proxy is "transparent" for such errors (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError)
|
||||
-- - other response/transport/connection errors from the client connected to proxy itself
|
||||
-- Other errors are reported in the function result as `Either ProxiedRelayError ()`, including
|
||||
-- - protocol errors from the client connected to proxy in ProxyClientError (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError)
|
||||
-- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError
|
||||
|
||||
proxySMPMessage ::
|
||||
SMPClient ->
|
||||
-- proxy session from PKEY
|
||||
ProxiedRelay ->
|
||||
-- message to deliver
|
||||
Maybe SndPrivateAuthKey ->
|
||||
SenderId ->
|
||||
MsgFlags ->
|
||||
MsgBody ->
|
||||
ExceptT SMPClientError IO (Either ProxyClientError ())
|
||||
proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do
|
||||
-- prepare params
|
||||
let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams
|
||||
serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth}
|
||||
(cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g
|
||||
let cmdSecret = C.dh' serverKey cmdPrivKey
|
||||
nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g
|
||||
-- encode
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender (SEND flags msg))
|
||||
auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth
|
||||
b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of
|
||||
[] -> throwE $ PCETransportError TELargeMsg
|
||||
TBError e _ : _ -> throwE $ PCETransportError e
|
||||
TBTransmission s _ : _ -> pure s
|
||||
TBTransmissions s _ _ : _ -> pure s
|
||||
et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength
|
||||
-- proxy interaction errors are wrapped
|
||||
let tOut = Just $ 2 * tcpTimeout
|
||||
tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case
|
||||
Right r -> case r of
|
||||
PRES (EncResponse er) -> do
|
||||
-- server interaction errors are thrown directly
|
||||
t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er
|
||||
case tParse serverThParams t' of
|
||||
t'' :| [] -> case tDecodeParseValidate serverThParams t'' of
|
||||
(_auth, _signed, (_c, _e, cmd)) -> case cmd of
|
||||
Right OK -> pure $ Right ()
|
||||
Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay
|
||||
Right r' -> throwE $ unexpectedResponse r'
|
||||
Left e -> throwE $ PCEResponseError e
|
||||
_ -> throwE $ PCETransportError TEBadBlock
|
||||
ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left
|
||||
_ -> pure . Left $ ProxyUnexpectedResponse $ take 32 $ show r
|
||||
Left e -> case e of
|
||||
PCEProtocolError e' -> pure . Left $ ProxyProtocolError e'
|
||||
PCEUnexpectedResponse e' -> pure . Left $ ProxyUnexpectedResponse $ B.unpack e'
|
||||
PCEResponseError e' -> pure . Left $ ProxyResponseError e'
|
||||
_ -> throwE e
|
||||
|
||||
-- this method is used in the proxy
|
||||
-- sends RFWD :: EncFwdTransmission -> Command Sender
|
||||
-- receives RRES :: EncFwdResponse -> BrokerMsg
|
||||
-- proxy should send PRES to the client with EncResponse
|
||||
forwardSMPMessage :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse
|
||||
forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do
|
||||
-- prepare params
|
||||
sessSecret <- case thAuth thParams of
|
||||
Nothing -> throwError $ PCETransportError TENoServerAuth
|
||||
Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret
|
||||
nonce <- liftIO . atomically $ C.randomCbNonce g
|
||||
-- wrap
|
||||
let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission}
|
||||
eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT)
|
||||
-- send
|
||||
sendProtocolCommand_ c (Just nonce) Nothing Nothing "" (Cmd SSender (RFWD eft)) >>= \case
|
||||
RRES (EncFwdResponse efr) -> do
|
||||
-- unwrap
|
||||
r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr
|
||||
FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r'
|
||||
pure fwdResponse
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
okSMPCommand cmd c pKey qId =
|
||||
sendSMPCommand c (Just pKey) qId cmd >>= \case
|
||||
OK -> return ()
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
okSMPCommands :: PartyI p => Command p -> SMPClient -> NonEmpty (C.APrivateAuthKey, QueueId) -> IO (NonEmpty (Either SMPClientError ()))
|
||||
okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs
|
||||
@@ -676,7 +924,7 @@ okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs
|
||||
cs = L.map (\(pKey, qId) -> (Just pKey, qId, aCmd)) qs
|
||||
process (Response _ r) = case r of
|
||||
Right OK -> Right ()
|
||||
Right r' -> Left . PCEUnexpectedResponse $ bshow r'
|
||||
Right r' -> Left $ unexpectedResponse r'
|
||||
Left e -> Left e
|
||||
|
||||
-- | Send SMP command
|
||||
@@ -687,7 +935,7 @@ sendSMPCommand c pKey qId cmd = sendProtocolCommand c pKey qId (Cmd sParty cmd)
|
||||
type PCTransmission err msg = (Either TransportError SentRawTransmission, Request err msg)
|
||||
|
||||
-- | Send multiple commands with batching and collect responses
|
||||
sendProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg))
|
||||
sendProtocolCommands :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg))
|
||||
sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs = do
|
||||
bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs
|
||||
validate . concat =<< mapM (sendBatch c) bs
|
||||
@@ -704,32 +952,33 @@ sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSiz
|
||||
where
|
||||
diff = L.length cs - length rs
|
||||
|
||||
streamProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO ()
|
||||
streamProtocolCommands :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO ()
|
||||
streamProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs cb = do
|
||||
bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs
|
||||
mapM_ (cb <=< sendBatch c) bs
|
||||
|
||||
sendBatch :: ProtocolClient v err msg -> TransportBatch (Request err msg) -> IO [Response err msg]
|
||||
sendBatch c@ProtocolClient {client_ = PClient {rcvConcurrency, sndQ}} b = do
|
||||
sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do
|
||||
case b of
|
||||
TBError e Request {entityId} -> do
|
||||
putStrLn "send error: large message"
|
||||
pure [Response entityId $ Left $ PCETransportError e]
|
||||
TBTransmissions s n rs
|
||||
| n > 0 -> do
|
||||
active <- newTVarIO True
|
||||
atomically $ writeTBQueue sndQ (active, s)
|
||||
mapConcurrently (getResponse c active) rs
|
||||
atomically $ writeTBQueue sndQ s
|
||||
mapConcurrently (getResponse c Nothing) rs
|
||||
| otherwise -> pure []
|
||||
TBTransmission s r -> do
|
||||
active <- newTVarIO True
|
||||
atomically $ writeTBQueue sndQ (active, s)
|
||||
(: []) <$> getResponse c active r
|
||||
atomically $ writeTBQueue sndQ s
|
||||
(: []) <$> getResponse c Nothing r
|
||||
|
||||
-- | Send Protocol command
|
||||
sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
|
||||
sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} pKey entId cmd =
|
||||
ExceptT $ uncurry sendRecv =<< mkTransmission c (pKey, entId, cmd)
|
||||
sendProtocolCommand :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
|
||||
sendProtocolCommand c = sendProtocolCommand_ c Nothing Nothing
|
||||
|
||||
sendProtocolCommand_ :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe Int -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
|
||||
sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ tOut pKey entId cmd =
|
||||
ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd)
|
||||
where
|
||||
-- two separate "atomically" needed to avoid blocking
|
||||
sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg)
|
||||
@@ -738,49 +987,59 @@ sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THand
|
||||
Right t
|
||||
| B.length s > blockSize - 2 -> pure . Left $ PCETransportError TELargeMsg
|
||||
| otherwise -> do
|
||||
active <- newTVarIO True
|
||||
atomically (writeTBQueue sndQ (active, s))
|
||||
response <$> getResponse c active r
|
||||
atomically $ writeTBQueue sndQ s
|
||||
response <$> getResponse c tOut r
|
||||
where
|
||||
s
|
||||
| batch = tEncodeBatch1 t
|
||||
| otherwise = tEncode t
|
||||
|
||||
-- TODO switch to timeout or TimeManager that supports Int64
|
||||
getResponse :: ProtocolClient v err msg -> TVar Bool -> Request err msg -> IO (Response err msg)
|
||||
getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount, sentCommands}} active Request {corrId, entityId, responseVar} = do
|
||||
response <-
|
||||
timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case
|
||||
Just r -> atomically (writeTVar timeoutErrorCount 0) $> r
|
||||
Nothing -> do
|
||||
atomically (writeTVar active False >> TM.delete corrId sentCommands)
|
||||
atomically $ modifyTVar' timeoutErrorCount (+ 1)
|
||||
pure $ Left PCEResponseTimeout
|
||||
getResponse :: ProtocolClient v err msg -> Maybe Int -> Request err msg -> IO (Response err msg)
|
||||
getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount}} tOut Request {entityId, pending, responseVar} = do
|
||||
r <- fromMaybe tcpTimeout tOut `timeout` atomically (takeTMVar responseVar)
|
||||
response <- atomically $ do
|
||||
writeTVar pending False
|
||||
-- Try to read response again in case it arrived after timeout expired
|
||||
-- but before `pending` was set to False above.
|
||||
-- See `processMsg`.
|
||||
((r <|>) <$> tryTakeTMVar responseVar) >>= \case
|
||||
Just r' -> writeTVar timeoutErrorCount 0 $> r'
|
||||
Nothing -> modifyTVar' timeoutErrorCount (+ 1) $> Left PCEResponseTimeout
|
||||
pure Response {entityId, response}
|
||||
|
||||
mkTransmission :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg)
|
||||
mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} (pKey_, entId, cmd) = do
|
||||
corrId <- atomically getNextCorrId
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, entId, cmd)
|
||||
auth = authTransmission (thAuth thParams) pKey_ corrId tForAuth
|
||||
r <- atomically $ mkRequest corrId
|
||||
mkTransmission :: Protocol v err msg => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg)
|
||||
mkTransmission c = mkTransmission_ c Nothing
|
||||
|
||||
mkTransmission_ :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.CbNonce -> ClientCommand msg -> IO (PCTransmission err msg)
|
||||
mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (pKey_, entityId, command) = do
|
||||
nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entityId, command)
|
||||
auth = authTransmission (thAuth thParams) pKey_ nonce tForAuth
|
||||
r <- atomically $ mkRequest (CorrId corrId)
|
||||
pure ((,tToSend) <$> auth, r)
|
||||
where
|
||||
getNextCorrId :: STM CorrId
|
||||
getNextCorrId = CorrId <$> C.randomBytes 24 clientCorrId -- also used as nonce
|
||||
mkRequest :: CorrId -> STM (Request err msg)
|
||||
mkRequest corrId = do
|
||||
r <- Request corrId entId <$> newEmptyTMVar
|
||||
pending <- newTVar True
|
||||
responseVar <- newEmptyTMVar
|
||||
let r =
|
||||
Request
|
||||
{ corrId,
|
||||
entityId,
|
||||
command,
|
||||
pending,
|
||||
responseVar
|
||||
}
|
||||
TM.insert corrId r sentCommands
|
||||
pure r
|
||||
|
||||
authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth)
|
||||
authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_
|
||||
authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth)
|
||||
authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_
|
||||
where
|
||||
authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth
|
||||
authenticate (C.APrivateAuthKey a pk) = case a of
|
||||
C.SX25519 -> case thAuth of
|
||||
Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t
|
||||
Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t
|
||||
Nothing -> Left TENoServerAuth
|
||||
C.SEd25519 -> sign pk
|
||||
C.SEd448 -> sign pk
|
||||
@@ -789,6 +1048,14 @@ authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "SM") ''SocksMode)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''NetworkConfig)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
@@ -14,6 +15,7 @@ module Simplex.Messaging.Client.Agent where
|
||||
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Concurrent.Async (Async, uninterruptibleCancel)
|
||||
import Control.Concurrent.STM (retry)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
@@ -33,30 +35,30 @@ import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Data.Set (Set)
|
||||
import Data.Text.Encoding
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime)
|
||||
import Data.Tuple (swap)
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer)
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, ErrorType, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer)
|
||||
import Simplex.Messaging.Session
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (catchAll_, toChunks, ($>>=))
|
||||
import Simplex.Messaging.Util (catchAll_, ifM, toChunks, whenM, ($>>=))
|
||||
import System.Timeout (timeout)
|
||||
import UnliftIO (async)
|
||||
import UnliftIO.Exception (Exception)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
type SMPClientVar = SessionVar (Either SMPClientError SMPClient)
|
||||
type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) (OwnServer, SMPClient))
|
||||
|
||||
data SMPClientAgentEvent
|
||||
= CAConnected SMPServer
|
||||
| CADisconnected SMPServer (Set SMPSub)
|
||||
| CAReconnected SMPServer
|
||||
| CAResubscribed SMPServer (NonEmpty SMPSub)
|
||||
| CASubError SMPServer (NonEmpty (SMPSub, SMPClientError))
|
||||
|
||||
@@ -70,9 +72,11 @@ type SMPSub = (SMPSubParty, QueueId)
|
||||
data SMPClientAgentConfig = SMPClientAgentConfig
|
||||
{ smpCfg :: ProtocolClientConfig SMPVersion,
|
||||
reconnectInterval :: RetryInterval,
|
||||
persistErrorInterval :: NominalDiffTime,
|
||||
msgQSize :: Natural,
|
||||
agentQSize :: Natural,
|
||||
agentSubsBatchSize :: Int
|
||||
agentSubsBatchSize :: Int,
|
||||
ownServerDomains :: [ByteString]
|
||||
}
|
||||
|
||||
defaultSMPClientAgentConfig :: SMPClientAgentConfig
|
||||
@@ -85,26 +89,31 @@ defaultSMPClientAgentConfig =
|
||||
increaseAfter = 10 * second,
|
||||
maxInterval = 10 * second
|
||||
},
|
||||
persistErrorInterval = 0,
|
||||
msgQSize = 256,
|
||||
agentQSize = 256,
|
||||
agentSubsBatchSize = 900
|
||||
agentSubsBatchSize = 900,
|
||||
ownServerDomains = []
|
||||
}
|
||||
where
|
||||
second = 1000000
|
||||
|
||||
data SMPClientAgent = SMPClientAgent
|
||||
{ agentCfg :: SMPClientAgentConfig,
|
||||
msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg),
|
||||
active :: TVar Bool,
|
||||
msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg),
|
||||
agentQ :: TBQueue SMPClientAgentEvent,
|
||||
randomDrg :: TVar ChaChaDRG,
|
||||
smpClients :: TMap SMPServer SMPClientVar,
|
||||
smpSessions :: TMap SessionId (OwnServer, SMPClient),
|
||||
srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
|
||||
pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
|
||||
reconnections :: TVar [Async ()],
|
||||
asyncClients :: TVar [Async ()],
|
||||
smpSubWorkers :: TMap SMPServer (SessionVar (Async ())),
|
||||
workerSeq :: TVar Int
|
||||
}
|
||||
|
||||
type OwnServer = Bool
|
||||
|
||||
newtype InternalException e = InternalException {unInternalException :: e}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -132,81 +141,98 @@ instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where
|
||||
|
||||
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent
|
||||
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do
|
||||
active <- newTVar True
|
||||
msgQ <- newTBQueue msgQSize
|
||||
agentQ <- newTBQueue agentQSize
|
||||
smpClients <- TM.empty
|
||||
smpSessions <- TM.empty
|
||||
srvSubs <- TM.empty
|
||||
pendingSrvSubs <- TM.empty
|
||||
reconnections <- newTVar []
|
||||
asyncClients <- newTVar []
|
||||
smpSubWorkers <- TM.empty
|
||||
workerSeq <- newTVar 0
|
||||
pure
|
||||
SMPClientAgent
|
||||
{ agentCfg,
|
||||
active,
|
||||
msgQ,
|
||||
agentQ,
|
||||
randomDrg,
|
||||
smpClients,
|
||||
smpSessions,
|
||||
srvSubs,
|
||||
pendingSrvSubs,
|
||||
reconnections,
|
||||
asyncClients,
|
||||
smpSubWorkers,
|
||||
workerSeq
|
||||
}
|
||||
|
||||
-- | Get or create SMP client for SMPServer
|
||||
getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient
|
||||
getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, workerSeq} srv =
|
||||
atomically getClientVar >>= either newSMPClient waitForSMPClient
|
||||
getSMPServerClient' ca srv = snd <$> getSMPServerClient'' ca srv
|
||||
{-# INLINE getSMPServerClient' #-}
|
||||
|
||||
getSMPServerClient'' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO (OwnServer, SMPClient)
|
||||
getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv =
|
||||
atomically getClientVar >>= either (ExceptT . newSMPClient) waitForSMPClient
|
||||
where
|
||||
getClientVar :: STM (Either SMPClientVar SMPClientVar)
|
||||
getClientVar = getSessVar workerSeq srv smpClients
|
||||
|
||||
waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
|
||||
waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO (OwnServer, SMPClient)
|
||||
waitForSMPClient v = do
|
||||
let ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg
|
||||
smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v)
|
||||
liftEither $ case smpClient_ of
|
||||
Just (Right smpClient) -> Right smpClient
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left PCEResponseTimeout
|
||||
case smpClient_ of
|
||||
Just (Right smpClient) -> pure smpClient
|
||||
Just (Left (e, Nothing)) -> throwE e
|
||||
Just (Left (e, Just ts)) ->
|
||||
ifM
|
||||
((ts <) <$> liftIO getCurrentTime)
|
||||
(atomically (removeSessVar v srv smpClients) >> getSMPServerClient'' ca srv)
|
||||
(throwE e)
|
||||
Nothing -> throwE PCEResponseTimeout
|
||||
|
||||
newSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
|
||||
newSMPClient v = tryConnectClient pure (liftIO tryConnectAsync)
|
||||
where
|
||||
tryConnectClient :: (SMPClient -> ExceptT SMPClientError IO a) -> ExceptT SMPClientError IO () -> ExceptT SMPClientError IO a
|
||||
tryConnectClient successAction retryAction =
|
||||
tryE (connectClient v) >>= \r -> case r of
|
||||
Right smp -> do
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
|
||||
atomically $ putTMVar (sessionVar v) r
|
||||
successAction smp
|
||||
Left e -> do
|
||||
if e == PCENetworkError || e == PCEResponseTimeout
|
||||
then retryAction
|
||||
else atomically $ do
|
||||
putTMVar (sessionVar v) (Left e)
|
||||
removeSessVar v srv smpClients
|
||||
throwE e
|
||||
tryConnectAsync :: IO ()
|
||||
tryConnectAsync = do
|
||||
a <- async $ void $ runExceptT connectAsync
|
||||
atomically $ modifyTVar' (asyncClients ca) (a :)
|
||||
connectAsync :: ExceptT SMPClientError IO ()
|
||||
connectAsync =
|
||||
withRetryInterval (reconnectInterval agentCfg) $ \_ loop ->
|
||||
void $ tryConnectClient (const reconnectClient) loop
|
||||
newSMPClient :: SMPClientVar -> IO (Either SMPClientError (OwnServer, SMPClient))
|
||||
newSMPClient v = do
|
||||
r <- connectClient ca srv v `E.catch` (pure . Left . PCEIOError)
|
||||
case r of
|
||||
Right smp -> do
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
|
||||
let c = (isOwnServer ca srv, smp)
|
||||
atomically $ do
|
||||
putTMVar (sessionVar v) (Right c)
|
||||
TM.insert (sessionId $ thParams smp) c smpSessions
|
||||
notify ca $ CAConnected srv
|
||||
pure $ Right c
|
||||
Left e -> do
|
||||
if persistErrorInterval agentCfg == 0 || e == PCENetworkError || e == PCEResponseTimeout
|
||||
then atomically $ do
|
||||
putTMVar (sessionVar v) (Left (e, Nothing))
|
||||
removeSessVar v srv smpClients
|
||||
else do
|
||||
ts <- addUTCTime (persistErrorInterval agentCfg) <$> liftIO getCurrentTime
|
||||
atomically $ putTMVar (sessionVar v) (Left (e, Just ts))
|
||||
reconnectClient ca srv
|
||||
pure $ Left e
|
||||
|
||||
connectClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
|
||||
connectClient v = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) (clientDisconnected v)
|
||||
isOwnServer :: SMPClientAgent -> SMPServer -> OwnServer
|
||||
isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} =
|
||||
let srv = strEncode $ L.head host
|
||||
in any (\s -> s == srv || (B.cons '.' s) `B.isSuffixOf` srv) (ownServerDomains agentCfg)
|
||||
|
||||
clientDisconnected :: SMPClientVar -> SMPClient -> IO ()
|
||||
clientDisconnected v _ = do
|
||||
removeClientAndSubs v >>= (`forM_` serverDown)
|
||||
-- | Run an SMP client for SMPClientVar
|
||||
connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient)
|
||||
connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg} srv v =
|
||||
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected
|
||||
where
|
||||
clientDisconnected :: SMPClient -> IO ()
|
||||
clientDisconnected smp = do
|
||||
removeClientAndSubs smp >>= (`forM_` serverDown)
|
||||
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
||||
|
||||
removeClientAndSubs :: SMPClientVar -> IO (Maybe (Map SMPSub C.APrivateAuthKey))
|
||||
removeClientAndSubs v = atomically $ do
|
||||
removeClientAndSubs :: SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey))
|
||||
removeClientAndSubs smp = atomically $ do
|
||||
removeSessVar v srv smpClients
|
||||
TM.delete (sessionId $ thParams smp) smpSessions
|
||||
TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs
|
||||
where
|
||||
updateSubs sVar = do
|
||||
@@ -222,67 +248,89 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg, wo
|
||||
|
||||
serverDown :: Map SMPSub C.APrivateAuthKey -> IO ()
|
||||
serverDown ss = unless (M.null ss) $ do
|
||||
notify . CADisconnected srv $ M.keysSet ss
|
||||
reconnectServer
|
||||
notify ca . CADisconnected srv $ M.keysSet ss
|
||||
reconnectClient ca srv
|
||||
|
||||
reconnectServer :: IO ()
|
||||
reconnectServer = do
|
||||
a <- async $ void $ runExceptT tryReconnectClient
|
||||
atomically $ modifyTVar' (reconnections ca) (a :)
|
||||
-- | Spawn reconnect worker if needed
|
||||
reconnectClient :: SMPClientAgent -> SMPServer -> IO ()
|
||||
reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} srv =
|
||||
whenM (readTVarIO active) $ atomically getWorkerVar >>= mapM_ (either newSubWorker (\_ -> pure ()))
|
||||
where
|
||||
getWorkerVar =
|
||||
ifM
|
||||
(null <$> getPending)
|
||||
(pure Nothing) -- prevent race with cleanup and adding pending queues in another call
|
||||
(Just <$> getSessVar workerSeq srv smpSubWorkers)
|
||||
newSubWorker :: SessionVar (Async ()) -> IO ()
|
||||
newSubWorker v = do
|
||||
a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v)
|
||||
atomically $ putTMVar (sessionVar v) a
|
||||
runSubWorker =
|
||||
withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> do
|
||||
pending <- atomically getPending
|
||||
forM_ pending $ \cs -> whenM (readTVarIO active) $ do
|
||||
void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv cs)
|
||||
loop
|
||||
ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg
|
||||
getPending = mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca)
|
||||
cleanup :: SessionVar (Async ()) -> STM ()
|
||||
cleanup v = do
|
||||
-- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar.
|
||||
-- Not waiting may result in terminated worker remaining in the map.
|
||||
whenM (isEmptyTMVar $ sessionVar v) retry
|
||||
removeSessVar v srv smpSubWorkers
|
||||
|
||||
tryReconnectClient :: ExceptT SMPClientError IO ()
|
||||
tryReconnectClient = do
|
||||
withRetryInterval (reconnectInterval agentCfg) $ \_ loop ->
|
||||
reconnectClient `catchE` const loop
|
||||
|
||||
reconnectClient :: ExceptT SMPClientError IO ()
|
||||
reconnectClient = do
|
||||
withSMP ca srv $ \smp -> do
|
||||
liftIO $ notify $ CAReconnected srv
|
||||
cs_ <- atomically $ mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca)
|
||||
forM_ cs_ $ \cs -> do
|
||||
subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs
|
||||
let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs'
|
||||
subscribe_ smp SPNotifier nSubs
|
||||
subscribe_ smp SPRecipient rSubs
|
||||
reconnectSMPClient :: SMPClientAgent -> SMPServer -> Map SMPSub C.APrivateAuthKey -> ExceptT SMPClientError IO ()
|
||||
reconnectSMPClient ca@SMPClientAgent {agentCfg} srv cs =
|
||||
withSMP ca srv $ \smp -> do
|
||||
subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs
|
||||
let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs'
|
||||
subscribe_ smp SPNotifier nSubs
|
||||
subscribe_ smp SPRecipient rSubs
|
||||
where
|
||||
isNotifier = \case
|
||||
SPNotifier -> True
|
||||
SPRecipient -> False
|
||||
subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO ()
|
||||
subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg)
|
||||
where
|
||||
isNotifier = \case
|
||||
SPNotifier -> True
|
||||
SPRecipient -> False
|
||||
subscribeBatch subs' = do
|
||||
let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs'
|
||||
rs <- liftIO $ smpSubscribeQueues party ca smp srv subs''
|
||||
let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) =
|
||||
L.zipWith (first . const) subs' rs
|
||||
rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] =
|
||||
map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs'
|
||||
(errs, oks) = partitionEithers rs''
|
||||
(tempErrs, finalErrs) = partition (temporaryClientError . snd) errs
|
||||
mapM_ (atomically . addSubscription ca srv) oks
|
||||
mapM_ (notify ca . CAResubscribed srv) $ L.nonEmpty $ map fst oks
|
||||
mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs
|
||||
mapM_ (notify ca . CASubError srv) $ L.nonEmpty finalErrs
|
||||
mapM_ (throwE . snd) $ listToMaybe tempErrs
|
||||
|
||||
subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO ()
|
||||
subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg)
|
||||
where
|
||||
subscribeBatch subs' = do
|
||||
let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs'
|
||||
rs <- liftIO $ smpSubscribeQueues party ca smp srv subs''
|
||||
let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) =
|
||||
L.zipWith (first . const) subs' rs
|
||||
rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] =
|
||||
map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs'
|
||||
(errs, oks) = partitionEithers rs''
|
||||
(tempErrs, finalErrs) = partition (temporaryClientError . snd) errs
|
||||
mapM_ (atomically . addSubscription ca srv) oks
|
||||
mapM_ (liftIO . notify . CAResubscribed srv) $ L.nonEmpty $ map fst oks
|
||||
mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs
|
||||
mapM_ (liftIO . notify . CASubError srv) $ L.nonEmpty finalErrs
|
||||
mapM_ (throwE . snd) $ listToMaybe tempErrs
|
||||
notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m ()
|
||||
notify ca evt = atomically $ writeTBQueue (agentQ ca) evt
|
||||
{-# INLINE notify #-}
|
||||
|
||||
notify :: SMPClientAgentEvent -> IO ()
|
||||
notify evt = atomically $ writeTBQueue (agentQ ca) evt
|
||||
lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe (OwnServer, SMPClient))
|
||||
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions
|
||||
|
||||
closeSMPClientAgent :: SMPClientAgent -> IO ()
|
||||
closeSMPClientAgent c = do
|
||||
atomically $ writeTVar (active c) False
|
||||
closeSMPServerClients c
|
||||
cancelActions $ reconnections c
|
||||
cancelActions $ asyncClients c
|
||||
atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect
|
||||
where
|
||||
cancelReconnect :: SessionVar (Async ()) -> IO ()
|
||||
cancelReconnect v = void . forkIO $ atomically (readTMVar $ sessionVar v) >>= uninterruptibleCancel
|
||||
|
||||
closeSMPServerClients :: SMPClientAgent -> IO ()
|
||||
closeSMPServerClients c = atomically (smpClients c `swapTVar` M.empty) >>= mapM_ (forkIO . closeClient)
|
||||
where
|
||||
closeClient v =
|
||||
atomically (readTMVar $ sessionVar v) >>= \case
|
||||
Right smp -> closeProtocolClient smp `catchAll_` pure ()
|
||||
Right (_, smp) -> closeProtocolClient smp `catchAll_` pure ()
|
||||
_ -> pure ()
|
||||
|
||||
cancelActions :: Foldable f => TVar (f (Async ())) -> IO ()
|
||||
|
||||
@@ -141,6 +141,7 @@ module Simplex.Messaging.Crypto
|
||||
sbEncrypt_,
|
||||
cbNonce,
|
||||
randomCbNonce,
|
||||
reverseNonce,
|
||||
|
||||
-- * NaCl crypto_secretbox
|
||||
SbKey (unSbKey),
|
||||
@@ -756,6 +757,8 @@ data Signature (a :: Algorithm) where
|
||||
SignatureEd25519 :: Ed25519.Signature -> Signature Ed25519
|
||||
SignatureEd448 :: Ed448.Signature -> Signature Ed448
|
||||
|
||||
deriving instance Eq (Signature a)
|
||||
|
||||
deriving instance Show (Signature a)
|
||||
|
||||
data ASignature
|
||||
@@ -1290,6 +1293,9 @@ randomCbNonce = fmap CryptoBoxNonce . randomBytes 24
|
||||
randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString
|
||||
randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n
|
||||
|
||||
reverseNonce :: CbNonce -> CbNonce
|
||||
reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s)
|
||||
|
||||
instance Encoding CbNonce where
|
||||
smpEncode = unCbNonce
|
||||
smpP = CryptoBoxNonce <$> A.take 24
|
||||
|
||||
@@ -117,7 +117,7 @@ import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, parseE, parseE')
|
||||
import Simplex.Messaging.Util ((<$?>), ($>>=))
|
||||
import Simplex.Messaging.Util (($>>=), (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
import UnliftIO.STM
|
||||
@@ -266,6 +266,7 @@ instance VersionRangeI E2EVersion (E2ERatchetParamsUri s a) where
|
||||
type VersionT E2EVersion (E2ERatchetParamsUri s a) = (E2ERatchetParams s a)
|
||||
versionRange (E2ERatchetParamsUri vr _ _ _) = vr
|
||||
toVersionT (E2ERatchetParamsUri _ k1 k2 kem_) v = E2ERatchetParams v k1 k2 kem_
|
||||
toVersionRange (E2ERatchetParamsUri _ k1 k2 kem_) vr = E2ERatchetParamsUri vr k1 k2 kem_
|
||||
|
||||
type RcvE2ERatchetParamsUri a = E2ERatchetParamsUri 'RKSProposed a
|
||||
|
||||
@@ -377,13 +378,15 @@ generateE2EParams g v useKEM_ = do
|
||||
where
|
||||
kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s))
|
||||
kemParams = case useKEM_ of
|
||||
Just useKem | v >= pqRatchetE2EEncryptVersion -> Just <$> do
|
||||
ks@(k, _) <- sntrup761Keypair g
|
||||
case useKem of
|
||||
ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks)
|
||||
AcceptKEM k' -> do
|
||||
(ct, shared) <- sntrup761Enc g k'
|
||||
pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks)
|
||||
Just useKem
|
||||
| v >= pqRatchetE2EEncryptVersion ->
|
||||
Just <$> do
|
||||
ks@(k, _) <- sntrup761Keypair g
|
||||
case useKem of
|
||||
ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks)
|
||||
AcceptKEM k' -> do
|
||||
(ct, shared) <- sntrup761Enc g k'
|
||||
pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks)
|
||||
_ -> pure Nothing
|
||||
|
||||
-- used by party initiating connection, Bob in double-ratchet spec
|
||||
@@ -456,7 +459,7 @@ pqX3dh (sk1, rk1) dh1 dh2 dh3 kemAccepted =
|
||||
pq = maybe "" (\RatchetKEMAccepted {rcPQRss = KEMSharedKey ss} -> BA.convert ss) kemAccepted
|
||||
(hk, nhk, sk) =
|
||||
let salt = B.replicate 64 '\0'
|
||||
in hkdf3 salt dhs "SimpleXX3DH"
|
||||
in hkdf3 salt dhs "SimpleXX3DH"
|
||||
|
||||
type RatchetX448 = Ratchet 'X448
|
||||
|
||||
@@ -698,8 +701,8 @@ data EncMessageHeader = EncMessageHeader
|
||||
|
||||
-- this encoding depends on version in EncMessageHeader because it is "current" ratchet version
|
||||
instance Encoding EncMessageHeader where
|
||||
smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody}
|
||||
= smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody
|
||||
smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} =
|
||||
smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody
|
||||
smpP = do
|
||||
(ehVersion, ehIV, ehAuthTag) <- smpP
|
||||
ehBody <- largeP
|
||||
@@ -708,8 +711,6 @@ instance Encoding EncMessageHeader where
|
||||
-- the encoder always uses 2-byte lengths for the new version, even for short headers without PQ keys.
|
||||
encodeLarge :: VersionE2E -> ByteString -> ByteString
|
||||
encodeLarge v s
|
||||
-- the condition for length is not necessary, it's here as a fallback.
|
||||
-- | v >= pqRatchetE2EEncryptVersion || B.length s > 255 = smpEncode $ Large s
|
||||
| v >= pqRatchetE2EEncryptVersion = smpEncode $ Large s
|
||||
| otherwise = smpEncode s
|
||||
|
||||
@@ -729,8 +730,8 @@ data EncRatchetMessage = EncRatchetMessage
|
||||
}
|
||||
|
||||
encodeEncRatchetMessage :: VersionE2E -> EncRatchetMessage -> ByteString
|
||||
encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag}
|
||||
= encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody)
|
||||
encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} =
|
||||
encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody)
|
||||
|
||||
encRatchetMessageP :: Parser EncRatchetMessage
|
||||
encRatchetMessageP = do
|
||||
|
||||
@@ -39,7 +39,7 @@ import Data.Time.Format.ISO8601
|
||||
import Data.Word (Word16, Word32)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
|
||||
|
||||
class TextEncoding a where
|
||||
textEncode :: a -> Text
|
||||
@@ -75,10 +75,16 @@ instance StrEncoding Str where
|
||||
strEncode = unStr
|
||||
strP = Str <$> A.takeTill (== ' ') <* optional A.space
|
||||
|
||||
-- inherited from ByteString, the parser only allows non-empty strings
|
||||
-- only Char8 elements may round-trip as B.pack truncates unicode
|
||||
instance StrEncoding String where
|
||||
strEncode = strEncode . B.pack
|
||||
strP = B.unpack <$> strP
|
||||
|
||||
instance StrEncoding Text where
|
||||
strEncode = strEncode . encodeUtf8
|
||||
strP = safeDecodeUtf8 <$> A.takeByteString
|
||||
|
||||
instance ToJSON Str where
|
||||
toJSON (Str s) = strToJSON s
|
||||
toEncoding (Str s) = strToJEncoding s
|
||||
|
||||
@@ -12,7 +12,6 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes)
|
||||
import Simplex.Messaging.Protocol (ErrorType)
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
|
||||
type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse
|
||||
|
||||
@@ -25,7 +24,7 @@ ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> Exc
|
||||
ntfRegisterToken c pKey newTkn =
|
||||
sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case
|
||||
NRTknId tknId dhKey -> pure (tknId, dhKey)
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
ntfVerifyToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> NtfRegCode -> ExceptT NtfClientError IO ()
|
||||
ntfVerifyToken c pKey tknId code = okNtfCommand (TVFY code) c pKey tknId
|
||||
@@ -34,7 +33,7 @@ ntfCheckToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> ExceptT NtfClie
|
||||
ntfCheckToken c pKey tknId =
|
||||
sendNtfCommand c (Just pKey) tknId TCHK >>= \case
|
||||
NRTkn stat -> pure stat
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
ntfReplaceToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> DeviceToken -> ExceptT NtfClientError IO ()
|
||||
ntfReplaceToken c pKey tknId token = okNtfCommand (TRPL token) c pKey tknId
|
||||
@@ -49,13 +48,13 @@ ntfCreateSubscription :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Subscri
|
||||
ntfCreateSubscription c pKey newSub =
|
||||
sendNtfCommand c (Just pKey) "" (SNEW newSub) >>= \case
|
||||
NRSubId subId -> pure subId
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
ntfCheckSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus
|
||||
ntfCheckSubscription c pKey subId =
|
||||
sendNtfCommand c (Just pKey) subId SCHK >>= \case
|
||||
NRSub stat -> pure stat
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
ntfDeleteSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO ()
|
||||
ntfDeleteSubscription = okNtfCommand SDEL
|
||||
@@ -68,4 +67,4 @@ okNtfCommand :: NtfEntityI e => NtfCommand e -> NtfClient -> C.APrivateAuthKey -
|
||||
okNtfCommand cmd c pKey entId =
|
||||
sendNtfCommand c (Just pKey) entId cmd >>= \case
|
||||
NROk -> return ()
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
@@ -31,7 +31,7 @@ import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
|
||||
import Data.Time.Clock.System (getSystemTime)
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Network.Socket (ServiceName)
|
||||
import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError)
|
||||
import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError, ServerTransmission (..))
|
||||
import Simplex.Messaging.Client.Agent
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -98,7 +98,9 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
|
||||
stopServer = do
|
||||
withNtfLog closeStoreLog
|
||||
saveServerStats
|
||||
asks (smpSubscribers . subscriber) >>= readTVarIO >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (liftIO . deRefWeak >=> mapM_ killThread))
|
||||
NtfSubscriber {smpSubscribers, smpAgent} <- asks subscriber
|
||||
liftIO $ readTVarIO smpSubscribers >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (deRefWeak >=> mapM_ killThread))
|
||||
liftIO $ closeSMPClientAgent smpAgent
|
||||
|
||||
serverStatsThread_ :: NtfServerConfig -> [M ()]
|
||||
serverStatsThread_ NtfServerConfig {logStatsInterval = Just interval, logStatsStartTime, serverStatsLogFile} =
|
||||
@@ -218,33 +220,38 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
|
||||
receiveSMP :: M ()
|
||||
receiveSMP = forever $ do
|
||||
((_, srv, _), _, _, _, ntfId, msg) <- atomically $ readTBQueue msgQ
|
||||
let smpQueue = SMPQueueNtf srv ntfId
|
||||
case msg of
|
||||
SMP.NMSG nmsgNonce encNMsgMeta -> do
|
||||
ntfTs <- liftIO getSystemTime
|
||||
st <- asks store
|
||||
NtfPushServer {pushQ} <- asks pushServer
|
||||
stats <- asks serverStats
|
||||
atomically $ updatePeriodStats (activeSubs stats) ntfId
|
||||
atomically $
|
||||
findNtfSubscriptionToken st smpQueue
|
||||
>>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}))
|
||||
incNtfStat ntfReceived
|
||||
SMP.END -> updateSubStatus smpQueue NSEnd
|
||||
_ -> pure ()
|
||||
((_, srv, _), _, _, ts) <- atomically $ readTBQueue msgQ
|
||||
forM ts $ \(ntfId, t) -> case t of
|
||||
STUnexpectedError e -> logError $ "SMP client unexpected error: " <> tshow e -- uncorrelated response, should not happen
|
||||
STResponse {} -> pure () -- it was already reported as timeout error
|
||||
STEvent msgOrErr -> do
|
||||
let smpQueue = SMPQueueNtf srv ntfId
|
||||
case msgOrErr of
|
||||
Right (SMP.NMSG nmsgNonce encNMsgMeta) -> do
|
||||
ntfTs <- liftIO getSystemTime
|
||||
st <- asks store
|
||||
NtfPushServer {pushQ} <- asks pushServer
|
||||
stats <- asks serverStats
|
||||
atomically $ updatePeriodStats (activeSubs stats) ntfId
|
||||
atomically $
|
||||
findNtfSubscriptionToken st smpQueue
|
||||
>>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}))
|
||||
incNtfStat ntfReceived
|
||||
Right SMP.END -> updateSubStatus smpQueue NSEnd
|
||||
Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e
|
||||
Right _ -> logError $ "SMP server unexpected response"
|
||||
Left e -> logError $ "SMP client error: " <> tshow e
|
||||
|
||||
receiveAgent =
|
||||
forever $
|
||||
atomically (readTBQueue agentQ) >>= \case
|
||||
CAConnected _ -> pure ()
|
||||
CAConnected srv ->
|
||||
logInfo $ "SMP server reconnected " <> showServer' srv
|
||||
CADisconnected srv subs -> do
|
||||
logSubStatus srv "disconnected" $ length subs
|
||||
forM_ subs $ \(_, ntfId) -> do
|
||||
let smpQueue = SMPQueueNtf srv ntfId
|
||||
updateSubStatus smpQueue NSInactive
|
||||
CAReconnected srv ->
|
||||
logInfo $ "SMP server reconnected " <> showServer' srv
|
||||
CAResubscribed srv subs -> do
|
||||
forM_ subs $ \(_, ntfId) -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive
|
||||
logSubStatus srv "resubscribed" $ length subs
|
||||
|
||||
@@ -11,6 +11,7 @@ module Simplex.Messaging.Notifications.Transport where
|
||||
|
||||
import Control.Monad (forM)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
@@ -116,9 +117,10 @@ ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do
|
||||
NtfClientHandshake {ntfVersion = v, keyHash}
|
||||
| keyHash /= kh ->
|
||||
throwError $ TEHandshake IDENTITY
|
||||
| v `isCompatible` ntfVersionRange ->
|
||||
pure $ ntfThHandleServer th v pk
|
||||
| otherwise -> throwError $ TEHandshake VERSION
|
||||
| otherwise ->
|
||||
case compatibleVRange' ntfVersionRange v of
|
||||
Just (Compatible vr) -> pure $ ntfThHandleServer th v vr pk
|
||||
Nothing -> throwE TEVersion
|
||||
|
||||
-- | Notifcations server client transport handshake.
|
||||
ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient)
|
||||
@@ -127,34 +129,45 @@ ntfClientHandshake c keyHash ntfVRange = do
|
||||
NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th
|
||||
if sessionId /= sessId
|
||||
then throwError TEBadSession
|
||||
else case ntfVersionRange `compatibleVersion` ntfVRange of
|
||||
Just (Compatible v) -> do
|
||||
else case ntfVersionRange `compatibleVRange` ntfVRange of
|
||||
Just (Compatible vr) -> do
|
||||
ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
|
||||
serverKey <- getServerVerifyKey c
|
||||
pubKey <- C.verifyX509 serverKey signedKey
|
||||
(,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
|
||||
let v = maxVersion vr
|
||||
sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash}
|
||||
pure $ ntfThHandleClient th v ck_
|
||||
Nothing -> throwError $ TEHandshake VERSION
|
||||
pure $ ntfThHandleClient th v vr ck_
|
||||
Nothing -> throwE TEVersion
|
||||
|
||||
ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer
|
||||
ntfThHandleServer th v pk =
|
||||
ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> VersionRangeNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer
|
||||
ntfThHandleServer th v vr pk =
|
||||
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing}
|
||||
in ntfThHandle_ th v (Just thAuth)
|
||||
in ntfThHandle_ th v vr (Just thAuth)
|
||||
|
||||
ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient
|
||||
ntfThHandleClient th v ck_ =
|
||||
ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> VersionRangeNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient
|
||||
ntfThHandleClient th v vr ck_ =
|
||||
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = Nothing}) <$> ck_
|
||||
in ntfThHandle_ th v thAuth
|
||||
in ntfThHandle_ th v vr thAuth
|
||||
|
||||
ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p
|
||||
ntfThHandle_ th@THandle {params} v thAuth =
|
||||
ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> VersionRangeNTF -> Maybe (THandleAuth p) -> THandleNTF c p
|
||||
ntfThHandle_ th@THandle {params} v vr thAuth =
|
||||
-- TODO drop SMP v6: make thAuth non-optional
|
||||
let v3 = v >= authBatchCmdsNTFVersion
|
||||
params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3}
|
||||
params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v3, batch = v3}
|
||||
in (th :: THandleNTF c p) {params = params'}
|
||||
|
||||
ntfTHandle :: Transport c => c -> THandleNTF c p
|
||||
ntfTHandle c = THandle {connection = c, params}
|
||||
where
|
||||
params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False}
|
||||
v = VersionNTF 0
|
||||
params =
|
||||
THandleParams
|
||||
{ sessionId = tlsUnique c,
|
||||
blockSize = ntfBlockSize,
|
||||
thVersion = v,
|
||||
thServerVRange = versionToRange v,
|
||||
thAuth = Nothing,
|
||||
implySessId = False,
|
||||
batch = False
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ module Simplex.Messaging.Protocol
|
||||
( -- * SMP protocol parameters
|
||||
supportedSMPClientVRange,
|
||||
maxMessageLength,
|
||||
paddedProxiedMsgLength,
|
||||
e2eEncConfirmationLength,
|
||||
e2eEncMessageLength,
|
||||
|
||||
@@ -56,6 +57,7 @@ module Simplex.Messaging.Protocol
|
||||
SubscriptionMode (..),
|
||||
Party (..),
|
||||
Cmd (..),
|
||||
DirectParty,
|
||||
BrokerMsg (..),
|
||||
SParty (..),
|
||||
PartyI (..),
|
||||
@@ -63,6 +65,8 @@ module Simplex.Messaging.Protocol
|
||||
ProtocolErrorType (..),
|
||||
ErrorType (..),
|
||||
CommandError (..),
|
||||
ProxyError (..),
|
||||
BrokerErrorType (..),
|
||||
Transmission,
|
||||
TransmissionAuth (..),
|
||||
SignedTransmission,
|
||||
@@ -121,6 +125,12 @@ module Simplex.Messaging.Protocol
|
||||
EncNMsgMeta,
|
||||
SMPMsgMeta (..),
|
||||
NMsgMeta (..),
|
||||
EncFwdResponse (..),
|
||||
EncFwdTransmission (..),
|
||||
EncResponse (..),
|
||||
EncTransmission (..),
|
||||
FwdResponse (..),
|
||||
FwdTransmission (..),
|
||||
MsgFlags (..),
|
||||
initialSMPClientVersion,
|
||||
userProtocol,
|
||||
@@ -167,6 +177,7 @@ module Simplex.Messaging.Protocol
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Exception (Exception)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
@@ -185,10 +196,15 @@ import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (isJust, isNothing)
|
||||
import Data.String
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock.System (SystemTime (..))
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word16)
|
||||
import qualified Data.X509 as X
|
||||
import GHC.TypeLits (ErrorMessage (..), TypeError, type (+))
|
||||
import qualified GHC.TypeLits as TE
|
||||
import qualified GHC.TypeLits as Type
|
||||
import Network.Socket (ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
@@ -197,7 +213,7 @@ import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.ServiceScheme
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..))
|
||||
import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>))
|
||||
import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
|
||||
@@ -228,9 +244,16 @@ currentSMPClientVersion = VersionSMPC 2
|
||||
supportedSMPClientVRange :: VersionRangeSMPC
|
||||
supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClientVersion
|
||||
|
||||
maxMessageLength :: Int
|
||||
maxMessageLength = 16088
|
||||
-- TODO v6.0 remove dependency on version
|
||||
maxMessageLength :: VersionSMP -> Int
|
||||
maxMessageLength v
|
||||
| v >= sendingProxySMPVersion = 16064 -- max 16067
|
||||
| otherwise = 16088 -- 16064 - always use this size to determine allowed ranges
|
||||
|
||||
paddedProxiedMsgLength :: Int
|
||||
paddedProxiedMsgLength = 16242 -- 16241 .. 16243
|
||||
|
||||
-- TODO v6.0 change to 16064
|
||||
type MaxMessageLen = 16088
|
||||
|
||||
-- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used)
|
||||
@@ -244,7 +267,7 @@ e2eEncMessageLength :: Int
|
||||
e2eEncMessageLength = 16016 -- 16004 .. 16021
|
||||
|
||||
-- | SMP protocol clients
|
||||
data Party = Recipient | Sender | Notifier
|
||||
data Party = Recipient | Sender | Notifier | ProxiedClient
|
||||
deriving (Show)
|
||||
|
||||
-- | Singleton types for SMP protocol clients
|
||||
@@ -252,11 +275,13 @@ data SParty :: Party -> Type where
|
||||
SRecipient :: SParty Recipient
|
||||
SSender :: SParty Sender
|
||||
SNotifier :: SParty Notifier
|
||||
SProxiedClient :: SParty ProxiedClient
|
||||
|
||||
instance TestEquality SParty where
|
||||
testEquality SRecipient SRecipient = Just Refl
|
||||
testEquality SSender SSender = Just Refl
|
||||
testEquality SNotifier SNotifier = Just Refl
|
||||
testEquality SProxiedClient SProxiedClient = Just Refl
|
||||
testEquality _ _ = Nothing
|
||||
|
||||
deriving instance Show (SParty p)
|
||||
@@ -269,6 +294,15 @@ instance PartyI Sender where sParty = SSender
|
||||
|
||||
instance PartyI Notifier where sParty = SNotifier
|
||||
|
||||
instance PartyI ProxiedClient where sParty = SProxiedClient
|
||||
|
||||
type family DirectParty (p :: Party) :: Constraint where
|
||||
DirectParty Recipient = ()
|
||||
DirectParty Sender = ()
|
||||
DirectParty Notifier = ()
|
||||
DirectParty p =
|
||||
(Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not direct"))
|
||||
|
||||
-- | Type for client command of any participant.
|
||||
data Cmd = forall p. PartyI p => Cmd (SParty p) (Command p)
|
||||
|
||||
@@ -359,6 +393,17 @@ data Command (p :: Party) where
|
||||
PING :: Command Sender
|
||||
-- SMP notification subscriber commands
|
||||
NSUB :: Command Notifier
|
||||
PRXY :: SMPServer -> Maybe BasicAuth -> Command ProxiedClient -- request a relay server connection by URI
|
||||
-- Transmission to proxy:
|
||||
-- - entity ID: ID of the session with relay returned in PKEY (response to PRXY)
|
||||
-- - corrId: also used as a nonce to encrypt transmission to relay, corrId + 1 - from relay
|
||||
-- - key (1st param in the command) is used to agree DH secret for this particular transmission and its response
|
||||
-- Encrypted transmission should include session ID (tlsunique) from proxy-relay connection.
|
||||
PFWD :: VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy
|
||||
-- Transmission forwarded to relay:
|
||||
-- - entity ID: empty
|
||||
-- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission
|
||||
RFWD :: EncFwdTransmission -> Command Sender -- use CorrId as CbNonce, proxy to relay
|
||||
|
||||
deriving instance Show (Command p)
|
||||
|
||||
@@ -384,6 +429,26 @@ instance Encoding SubscriptionMode where
|
||||
'C' -> pure SMOnlyCreate
|
||||
_ -> fail "bad SubscriptionMode"
|
||||
|
||||
newtype EncTransmission = EncTransmission ByteString
|
||||
deriving (Show)
|
||||
|
||||
data FwdTransmission = FwdTransmission
|
||||
{ fwdCorrId :: CorrId,
|
||||
fwdVersion :: VersionSMP,
|
||||
fwdKey :: C.PublicKeyX25519,
|
||||
fwdTransmission :: EncTransmission
|
||||
}
|
||||
|
||||
instance Encoding FwdTransmission where
|
||||
smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} =
|
||||
smpEncode (corrId, fwdVersion, fwdKey, Tail t)
|
||||
smpP = do
|
||||
(corrId, fwdVersion, fwdKey, Tail t) <- smpP
|
||||
pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t}
|
||||
|
||||
newtype EncFwdTransmission = EncFwdTransmission ByteString
|
||||
deriving (Show)
|
||||
|
||||
data BrokerMsg where
|
||||
-- SMP broker messages (responses, client messages, notifications)
|
||||
IDS :: QueueIdsKeys -> BrokerMsg
|
||||
@@ -393,6 +458,10 @@ data BrokerMsg where
|
||||
MSG :: RcvMessage -> BrokerMsg
|
||||
NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg
|
||||
NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg
|
||||
-- Should include certificate chain
|
||||
PKEY :: SessionId -> VersionRangeSMP -> (X.CertificateChain, X.SignedExact X.PubKey) -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key
|
||||
RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy
|
||||
PRES :: EncResponse -> BrokerMsg -- proxy to client
|
||||
END :: BrokerMsg
|
||||
OK :: BrokerMsg
|
||||
ERR :: ErrorType -> BrokerMsg
|
||||
@@ -405,6 +474,24 @@ data RcvMessage = RcvMessage
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
newtype EncFwdResponse = EncFwdResponse ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
data FwdResponse = FwdResponse
|
||||
{ fwdCorrId :: CorrId,
|
||||
fwdResponse :: EncResponse
|
||||
}
|
||||
|
||||
instance Encoding FwdResponse where
|
||||
smpEncode FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} =
|
||||
smpEncode (corrId, Tail t)
|
||||
smpP = do
|
||||
(corrId, Tail t) <- smpP
|
||||
pure FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t}
|
||||
|
||||
newtype EncResponse = EncResponse ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | received message without server/recipient encryption
|
||||
data Message
|
||||
= Message
|
||||
@@ -567,6 +654,9 @@ data CommandTag (p :: Party) where
|
||||
DEL_ :: CommandTag Recipient
|
||||
SEND_ :: CommandTag Sender
|
||||
PING_ :: CommandTag Sender
|
||||
PRXY_ :: CommandTag ProxiedClient
|
||||
PFWD_ :: CommandTag ProxiedClient
|
||||
RFWD_ :: CommandTag Sender
|
||||
NSUB_ :: CommandTag Notifier
|
||||
|
||||
data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p)
|
||||
@@ -580,6 +670,9 @@ data BrokerMsgTag
|
||||
| MSG_
|
||||
| NID_
|
||||
| NMSG_
|
||||
| PKEY_
|
||||
| RRES_
|
||||
| PRES_
|
||||
| END_
|
||||
| OK_
|
||||
| ERR_
|
||||
@@ -607,6 +700,9 @@ instance PartyI p => Encoding (CommandTag p) where
|
||||
DEL_ -> "DEL"
|
||||
SEND_ -> "SEND"
|
||||
PING_ -> "PING"
|
||||
PRXY_ -> "PRXY"
|
||||
PFWD_ -> "PFWD"
|
||||
RFWD_ -> "RFWD"
|
||||
NSUB_ -> "NSUB"
|
||||
smpP = messageTagP
|
||||
|
||||
@@ -623,6 +719,9 @@ instance ProtocolMsgTag CmdTag where
|
||||
"DEL" -> Just $ CT SRecipient DEL_
|
||||
"SEND" -> Just $ CT SSender SEND_
|
||||
"PING" -> Just $ CT SSender PING_
|
||||
"PRXY" -> Just $ CT SProxiedClient PRXY_
|
||||
"PFWD" -> Just $ CT SProxiedClient PFWD_
|
||||
"RFWD" -> Just $ CT SSender RFWD_
|
||||
"NSUB" -> Just $ CT SNotifier NSUB_
|
||||
_ -> Nothing
|
||||
|
||||
@@ -639,6 +738,9 @@ instance Encoding BrokerMsgTag where
|
||||
MSG_ -> "MSG"
|
||||
NID_ -> "NID"
|
||||
NMSG_ -> "NMSG"
|
||||
PKEY_ -> "PKEY"
|
||||
RRES_ -> "RRES"
|
||||
PRES_ -> "PRES"
|
||||
END_ -> "END"
|
||||
OK_ -> "OK"
|
||||
ERR_ -> "ERR"
|
||||
@@ -651,6 +753,9 @@ instance ProtocolMsgTag BrokerMsgTag where
|
||||
"MSG" -> Just MSG_
|
||||
"NID" -> Just NID_
|
||||
"NMSG" -> Just NMSG_
|
||||
"PKEY" -> Just PKEY_
|
||||
"RRES" -> Just RRES_
|
||||
"PRES" -> Just PRES_
|
||||
"END" -> Just END_
|
||||
"OK" -> Just OK_
|
||||
"ERR" -> Just ERR_
|
||||
@@ -829,7 +934,7 @@ type family UserProtocol (p :: ProtocolType) :: Constraint where
|
||||
UserProtocol PSMP = ()
|
||||
UserProtocol PXFTP = ()
|
||||
UserProtocol a =
|
||||
(Int ~ Bool, TypeError (Text "Servers for protocol " :<>: ShowType a :<>: Text " cannot be configured by the users"))
|
||||
(Int ~ Bool, TypeError (TE.Text "Servers for protocol " :<>: ShowType a :<>: TE.Text " cannot be configured by the users"))
|
||||
|
||||
userProtocol :: SProtocolType p -> Maybe (Dict (UserProtocol p))
|
||||
userProtocol = \case
|
||||
@@ -1038,14 +1143,20 @@ data ErrorType
|
||||
SESSION
|
||||
| -- | SMP command is unknown or has invalid syntax
|
||||
CMD {cmdErr :: CommandError}
|
||||
| -- | error from proxied relay
|
||||
PROXY {proxyErr :: ProxyError}
|
||||
| -- | command authorization error - bad signature or non-existing SMP queue
|
||||
AUTH
|
||||
| -- | encryption/decryption error in proxy protocol
|
||||
CRYPTO
|
||||
| -- | SMP queue capacity is exceeded on the server
|
||||
QUOTA
|
||||
| -- | ACK command is sent without message to be acknowledged
|
||||
NO_MSG
|
||||
| -- | sent message is too large (> maxMessageLength = 16088 bytes)
|
||||
LARGE_MSG
|
||||
| -- | relay public key is expired
|
||||
EXPIRED
|
||||
| -- | internal server error
|
||||
INTERNAL
|
||||
| -- | used internally, never returned by the server (to be removed)
|
||||
@@ -1055,8 +1166,12 @@ data ErrorType
|
||||
instance StrEncoding ErrorType where
|
||||
strEncode = \case
|
||||
CMD e -> "CMD " <> bshow e
|
||||
PROXY e -> "PROXY " <> strEncode e
|
||||
e -> bshow e
|
||||
strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
|
||||
strP =
|
||||
"CMD " *> (CMD <$> parseRead1)
|
||||
<|> "PROXY " *> (PROXY <$> strP)
|
||||
<|> parseRead1
|
||||
|
||||
-- | SMP command error type.
|
||||
data CommandError
|
||||
@@ -1074,6 +1189,34 @@ data CommandError
|
||||
NO_ENTITY
|
||||
deriving (Eq, Read, Show)
|
||||
|
||||
data ProxyError
|
||||
= -- | Correctly parsed SMP server ERR response.
|
||||
-- This error is forwarded to the agent client as AgentErrorType `ERR PROXY PROTOCOL err`.
|
||||
PROTOCOL {protocolErr :: ErrorType}
|
||||
| -- | destination server error
|
||||
BROKER {brokerErr :: BrokerErrorType}
|
||||
| -- | basic auth provided to proxy is invalid
|
||||
BASIC_AUTH
|
||||
| -- no destination server error
|
||||
NO_SESSION
|
||||
deriving (Eq, Read, Show)
|
||||
|
||||
-- | SMP server errors.
|
||||
data BrokerErrorType
|
||||
= -- | invalid server response (failed to parse)
|
||||
RESPONSE {respErr :: String}
|
||||
| -- | unexpected response
|
||||
UNEXPECTED {respErr :: String}
|
||||
| -- | network error
|
||||
NETWORK
|
||||
| -- | no compatible server host (e.g. onion when public is required, or vice versa)
|
||||
HOST
|
||||
| -- | handshake or other transport error
|
||||
TRANSPORT {transportErr :: TransportError}
|
||||
| -- | command response timeout
|
||||
TIMEOUT
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | SMP transmission parser.
|
||||
transmissionP :: THandleParams v p -> Parser RawTransmission
|
||||
transmissionP THandleParams {sessionId, implySessId} = do
|
||||
@@ -1135,6 +1278,9 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
|
||||
SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg)
|
||||
PING -> e PING_
|
||||
NSUB -> e NSUB_
|
||||
PRXY host auth_ -> e (PRXY_, ' ', host, auth_)
|
||||
PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s)
|
||||
RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s)
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
@@ -1144,24 +1290,33 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
|
||||
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (auth, _, queueId, _) cmd = case cmd of
|
||||
checkCredentials (auth, _, entId, _) cmd = case cmd of
|
||||
-- NEW must have signature but NOT queue ID
|
||||
NEW {}
|
||||
| isNothing auth -> Left $ CMD NO_AUTH
|
||||
| not (B.null queueId) -> Left $ CMD HAS_AUTH
|
||||
| not (B.null entId) -> Left $ CMD HAS_AUTH
|
||||
| otherwise -> Right cmd
|
||||
-- SEND must have queue ID, signature is not always required
|
||||
SEND {}
|
||||
| B.null queueId -> Left $ CMD NO_ENTITY
|
||||
| B.null entId -> Left $ CMD NO_ENTITY
|
||||
| otherwise -> Right cmd
|
||||
-- PING must not have queue ID or signature
|
||||
PING
|
||||
| isNothing auth && B.null queueId -> Right cmd
|
||||
PING -> noAuthCmd
|
||||
PRXY {} -> noAuthCmd
|
||||
PFWD {}
|
||||
| B.null entId -> Left $ CMD NO_ENTITY
|
||||
| isNothing auth -> Right cmd
|
||||
| otherwise -> Left $ CMD HAS_AUTH
|
||||
RFWD _ -> noAuthCmd
|
||||
-- other client commands must have both signature and queue ID
|
||||
_
|
||||
| isNothing auth || B.null queueId -> Left $ CMD NO_AUTH
|
||||
| isNothing auth || B.null entId -> Left $ CMD NO_AUTH
|
||||
| otherwise -> Right cmd
|
||||
where
|
||||
-- command must not have entity ID (queue or session ID) or signature
|
||||
noAuthCmd :: Either ErrorType (Command p)
|
||||
noAuthCmd
|
||||
| isNothing auth && B.null entId = Right cmd
|
||||
| otherwise = Left $ CMD HAS_AUTH
|
||||
|
||||
instance ProtocolEncoding SMPVersion ErrorType Cmd where
|
||||
type Tag Cmd = CmdTag
|
||||
@@ -1189,6 +1344,11 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
|
||||
Cmd SSender <$> case tag of
|
||||
SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP)
|
||||
PING_ -> pure PING
|
||||
RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP)
|
||||
CT SProxiedClient tag ->
|
||||
Cmd SProxiedClient <$> case tag of
|
||||
PFWD_ -> PFWD <$> _smpP <*> smpP <*> (EncTransmission . unTail <$> smpP)
|
||||
PRXY_ -> PRXY <$> _smpP <*> smpP
|
||||
CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB
|
||||
|
||||
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
||||
@@ -1204,6 +1364,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
e (MSG_, ' ', msgId, Tail body)
|
||||
NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh)
|
||||
NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta)
|
||||
PKEY sid vr (cert, key) -> e (PKEY_, ' ', sid, vr, C.encodeCertChain cert, C.SignedObject key)
|
||||
RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock)
|
||||
PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock)
|
||||
END -> e END_
|
||||
OK -> e OK_
|
||||
ERR err -> e (ERR_, ' ', err)
|
||||
@@ -1221,6 +1384,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP)
|
||||
NID_ -> NID <$> _smpP <*> smpP
|
||||
NMSG_ -> NMSG <$> _smpP <*> smpP
|
||||
PKEY_ -> PKEY <$> _smpP <*> smpP <*> ((,) <$> C.certChainP <*> (C.getSignedExact <$> smpP))
|
||||
RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP)
|
||||
PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP)
|
||||
END_ -> pure END
|
||||
OK_ -> pure OK
|
||||
ERR_ -> ERR <$> _smpP
|
||||
@@ -1233,19 +1399,24 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
PEBlock -> BLOCK
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (_, _, queueId, _) cmd = case cmd of
|
||||
checkCredentials (_, _, entId, _) cmd = case cmd of
|
||||
-- IDS response should not have queue ID
|
||||
IDS _ -> Right cmd
|
||||
-- ERR response does not always have queue ID
|
||||
ERR _ -> Right cmd
|
||||
-- PONG response must not have queue ID
|
||||
PONG
|
||||
| B.null queueId -> Right cmd
|
||||
| otherwise -> Left $ CMD HAS_AUTH
|
||||
PONG -> noEntityMsg
|
||||
PKEY {} -> noEntityMsg
|
||||
RRES _ -> noEntityMsg
|
||||
-- other broker responses must have queue ID
|
||||
_
|
||||
| B.null queueId -> Left $ CMD NO_ENTITY
|
||||
| B.null entId -> Left $ CMD NO_ENTITY
|
||||
| otherwise -> Right cmd
|
||||
where
|
||||
noEntityMsg :: Either ErrorType BrokerMsg
|
||||
noEntityMsg
|
||||
| B.null entId = Right cmd
|
||||
| otherwise = Left $ CMD HAS_AUTH
|
||||
|
||||
-- | Parse SMP protocol commands and broker messages
|
||||
parseProtocol :: forall v err msg. ProtocolEncoding v err msg => Version v -> ByteString -> Either err msg
|
||||
@@ -1268,8 +1439,11 @@ instance Encoding ErrorType where
|
||||
BLOCK -> "BLOCK"
|
||||
SESSION -> "SESSION"
|
||||
CMD err -> "CMD " <> smpEncode err
|
||||
PROXY err -> "PROXY " <> smpEncode err
|
||||
AUTH -> "AUTH"
|
||||
CRYPTO -> "CRYPTO"
|
||||
QUOTA -> "QUOTA"
|
||||
EXPIRED -> "EXPIRED"
|
||||
NO_MSG -> "NO_MSG"
|
||||
LARGE_MSG -> "LARGE_MSG"
|
||||
INTERNAL -> "INTERNAL"
|
||||
@@ -1280,13 +1454,16 @@ instance Encoding ErrorType where
|
||||
"BLOCK" -> pure BLOCK
|
||||
"SESSION" -> pure SESSION
|
||||
"CMD" -> CMD <$> _smpP
|
||||
"PROXY" -> PROXY <$> _smpP
|
||||
"AUTH" -> pure AUTH
|
||||
"CRYPTO" -> pure CRYPTO
|
||||
"QUOTA" -> pure QUOTA
|
||||
"EXPIRED" -> pure EXPIRED
|
||||
"NO_MSG" -> pure NO_MSG
|
||||
"LARGE_MSG" -> pure LARGE_MSG
|
||||
"INTERNAL" -> pure INTERNAL
|
||||
"DUPLICATE_" -> pure DUPLICATE_
|
||||
_ -> fail "bad error type"
|
||||
_ -> fail "bad ErrorType"
|
||||
|
||||
instance Encoding CommandError where
|
||||
smpEncode e = case e of
|
||||
@@ -1304,8 +1481,74 @@ instance Encoding CommandError where
|
||||
"NO_AUTH" -> pure NO_AUTH
|
||||
"HAS_AUTH" -> pure HAS_AUTH
|
||||
"NO_ENTITY" -> pure NO_ENTITY
|
||||
"NO_QUEUE" -> pure NO_ENTITY
|
||||
_ -> fail "bad command error type"
|
||||
"NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility
|
||||
_ -> fail "bad CommandError"
|
||||
|
||||
instance Encoding ProxyError where
|
||||
smpEncode = \case
|
||||
PROTOCOL e -> "PROTOCOL " <> smpEncode e
|
||||
BROKER e -> "BROKER " <> smpEncode e
|
||||
BASIC_AUTH -> "BASIC_AUTH"
|
||||
NO_SESSION -> "NO_SESSION"
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"PROTOCOL" -> PROTOCOL <$> _smpP
|
||||
"BROKER" -> BROKER <$> _smpP
|
||||
"BASIC_AUTH" -> pure BASIC_AUTH
|
||||
"NO_SESSION" -> pure NO_SESSION
|
||||
_ -> fail "bad ProxyError"
|
||||
|
||||
instance StrEncoding ProxyError where
|
||||
strEncode = \case
|
||||
PROTOCOL e -> "PROTOCOL " <> strEncode e
|
||||
BROKER e -> "BROKER " <> strEncode e
|
||||
BASIC_AUTH -> "BASIC_AUTH"
|
||||
NO_SESSION -> "NO_SESSION"
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"PROTOCOL" -> PROTOCOL <$> _strP
|
||||
"BROKER" -> BROKER <$> _strP
|
||||
"BASIC_AUTH" -> pure BASIC_AUTH
|
||||
"NO_SESSION" -> pure NO_SESSION
|
||||
_ -> fail "bad ProxyError"
|
||||
|
||||
instance Encoding BrokerErrorType where
|
||||
smpEncode = \case
|
||||
RESPONSE e -> "RESPONSE " <> smpEncode e
|
||||
UNEXPECTED e -> "UNEXPECTED " <> smpEncode e
|
||||
TRANSPORT e -> "TRANSPORT " <> smpEncode e
|
||||
NETWORK -> "NETWORK"
|
||||
TIMEOUT -> "TIMEOUT"
|
||||
HOST -> "HOST"
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"RESPONSE" -> RESPONSE <$> _smpP
|
||||
"UNEXPECTED" -> UNEXPECTED <$> _smpP
|
||||
"TRANSPORT" -> TRANSPORT <$> _smpP
|
||||
"NETWORK" -> pure NETWORK
|
||||
"TIMEOUT" -> pure TIMEOUT
|
||||
"HOST" -> pure HOST
|
||||
_ -> fail "bad BrokerErrorType"
|
||||
|
||||
instance StrEncoding BrokerErrorType where
|
||||
strEncode = \case
|
||||
RESPONSE e -> "RESPONSE " <> encodeUtf8 (T.pack e)
|
||||
UNEXPECTED e -> "UNEXPECTED " <> encodeUtf8 (T.pack e)
|
||||
TRANSPORT e -> "TRANSPORT " <> smpEncode e
|
||||
NETWORK -> "NETWORK"
|
||||
TIMEOUT -> "TIMEOUT"
|
||||
HOST -> "HOST"
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"RESPONSE" -> RESPONSE <$> _textP
|
||||
"UNEXPECTED" -> UNEXPECTED <$> _textP
|
||||
"TRANSPORT" -> TRANSPORT <$> _smpP
|
||||
"NETWORK" -> pure NETWORK
|
||||
"TIMEOUT" -> pure TIMEOUT
|
||||
"HOST" -> pure HOST
|
||||
_ -> fail "bad BrokerErrorType"
|
||||
where
|
||||
_textP = A.space *> (T.unpack . safeDecodeUtf8 <$> A.takeByteString)
|
||||
|
||||
-- | Send signed SMP transmission to TCP transport.
|
||||
tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()]
|
||||
@@ -1444,4 +1687,7 @@ $(J.deriveJSON defaultJSON ''MsgFlags)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''CommandError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''ErrorType)
|
||||
$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType)
|
||||
|
||||
-- run deriveJSON in one TH splice to allow mutual instance
|
||||
$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType])
|
||||
|
||||
+222
-59
@@ -6,6 +6,7 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
@@ -13,7 +14,6 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Server
|
||||
@@ -43,6 +43,7 @@ import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Random
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Base64 (encode)
|
||||
@@ -54,7 +55,7 @@ import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import Data.List (intercalate, mapAccumR)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (isNothing)
|
||||
@@ -68,8 +69,10 @@ import GHC.Stats (getRTSStats)
|
||||
import GHC.TypeLits (KnownNat)
|
||||
import Network.Socket (ServiceName, Socket, socketToHandle)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), forwardSMPMessage, smpProxyError, temporaryClientError)
|
||||
import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding (Encoding (smpEncode))
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.Control
|
||||
@@ -87,10 +90,12 @@ import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Buffer (trimCR)
|
||||
import Simplex.Messaging.Transport.Server
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import System.Exit (exitFailure)
|
||||
import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode)
|
||||
import System.Mem.Weak (deRefWeak)
|
||||
import UnliftIO (timeout)
|
||||
import UnliftIO.Async (mapConcurrently)
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Directory (doesFileExist, renameFile)
|
||||
import UnliftIO.Exception
|
||||
@@ -123,14 +128,16 @@ type M a = ReaderT Env IO a
|
||||
smpServer :: TMVar Bool -> ServerConfig -> M ()
|
||||
smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
s <- asks server
|
||||
pa <- asks proxyAgent
|
||||
expired <- restoreServerMessages
|
||||
restoreServerStats expired
|
||||
raceAny_
|
||||
( serverThread s "server subscribedQ" subscribedQ subscribers subscriptions cancelSub
|
||||
: serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubscriptions (\_ -> pure ())
|
||||
: receiveFromProxyAgent pa
|
||||
: map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg
|
||||
)
|
||||
`finally` withLock' (savingLock s) "final" (saveServer False)
|
||||
`finally` withLock' (savingLock s) "final" (saveServer False >> closeServer)
|
||||
where
|
||||
runServer :: (ServiceName, ATransport) -> M ()
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
@@ -144,6 +151,9 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
saveServer :: Bool -> M ()
|
||||
saveServer keepMsgs = withLog closeStoreLog >> saveServerMessages keepMsgs >> saveServerStats
|
||||
|
||||
closeServer :: M ()
|
||||
closeServer = asks (smpAgent . proxyAgent) >>= liftIO . closeSMPClientAgent
|
||||
|
||||
serverThread ::
|
||||
forall s.
|
||||
Server ->
|
||||
@@ -180,6 +190,18 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
mkWeakThreadId t >>= atomically . modifyTVar' (endThreads c) . IM.insert tId
|
||||
atomically $ TM.lookupDelete qId (clientSubs c)
|
||||
|
||||
receiveFromProxyAgent :: ProxyAgent -> M ()
|
||||
receiveFromProxyAgent ProxyAgent {smpAgent = SMPClientAgent {agentQ}} =
|
||||
forever $
|
||||
atomically (readTBQueue agentQ) >>= \case
|
||||
CAConnected srv -> logInfo $ "SMP server connected " <> showServer' srv
|
||||
CADisconnected srv [] -> logInfo $ "SMP server disconnected " <> showServer' srv
|
||||
CADisconnected srv subs -> logError $ "SMP server disconnected " <> showServer' srv <> " / subscriptions: " <> tshow (length subs)
|
||||
CAResubscribed srv subs -> logError $ "SMP server resubscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs)
|
||||
CASubError srv errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs)
|
||||
where
|
||||
showServer' = decodeLatin1 . strEncode . host
|
||||
|
||||
expireMessagesThread_ :: ServerConfig -> [M ()]
|
||||
expireMessagesThread_ ServerConfig {messageExpiration = Just msgExp} = [expireMessages msgExp]
|
||||
expireMessagesThread_ _ = []
|
||||
@@ -211,7 +233,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime
|
||||
liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath
|
||||
liftIO $ threadDelay' $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0)
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount} <- asks serverStats
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv} <- asks serverStats
|
||||
let interval = 1000000 * logInterval
|
||||
forever $ do
|
||||
withFile statsFilePath AppendMode $ \h -> liftIO $ do
|
||||
@@ -230,32 +252,46 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
msgSentNtf' <- atomically $ swapTVar msgSentNtf 0
|
||||
msgRecvNtf' <- atomically $ swapTVar msgRecvNtf 0
|
||||
psNtf <- atomically $ periodStatCounts activeQueuesNtf ts
|
||||
pRelays' <- atomically $ getResetProxyStatsData pRelays
|
||||
pRelaysOwn' <- atomically $ getResetProxyStatsData pRelaysOwn
|
||||
pMsgFwds' <- atomically $ getResetProxyStatsData pMsgFwds
|
||||
pMsgFwdsOwn' <- atomically $ getResetProxyStatsData pMsgFwdsOwn
|
||||
pMsgFwdsRecv' <- atomically $ swapTVar pMsgFwdsRecv 0
|
||||
qCount' <- readTVarIO qCount
|
||||
msgCount' <- readTVarIO msgCount
|
||||
hPutStrLn h $
|
||||
intercalate
|
||||
","
|
||||
[ iso8601Show $ utctDay fromTime',
|
||||
show qCreated',
|
||||
show qSecured',
|
||||
show qDeletedAll',
|
||||
show msgSent',
|
||||
show msgRecv',
|
||||
dayCount ps,
|
||||
weekCount ps,
|
||||
monthCount ps,
|
||||
show msgSentNtf',
|
||||
show msgRecvNtf',
|
||||
dayCount psNtf,
|
||||
weekCount psNtf,
|
||||
monthCount psNtf,
|
||||
show qCount',
|
||||
show msgCount',
|
||||
show msgExpired',
|
||||
show qDeletedNew',
|
||||
show qDeletedSecured'
|
||||
]
|
||||
( [ iso8601Show $ utctDay fromTime',
|
||||
show qCreated',
|
||||
show qSecured',
|
||||
show qDeletedAll',
|
||||
show msgSent',
|
||||
show msgRecv',
|
||||
dayCount ps,
|
||||
weekCount ps,
|
||||
monthCount ps,
|
||||
show msgSentNtf',
|
||||
show msgRecvNtf',
|
||||
dayCount psNtf,
|
||||
weekCount psNtf,
|
||||
monthCount psNtf,
|
||||
show qCount',
|
||||
show msgCount',
|
||||
show msgExpired',
|
||||
show qDeletedNew',
|
||||
show qDeletedSecured'
|
||||
]
|
||||
<> showProxyStats pRelays'
|
||||
<> showProxyStats pRelaysOwn'
|
||||
<> showProxyStats pMsgFwds'
|
||||
<> showProxyStats pMsgFwdsOwn'
|
||||
<> [show pMsgFwdsRecv']
|
||||
)
|
||||
liftIO $ threadDelay' interval
|
||||
where
|
||||
showProxyStats ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} =
|
||||
[show _pRequests, show _pSuccesses, show _pErrorsConnect, show _pErrorsCompat, show _pErrorsOther]
|
||||
|
||||
runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M ()
|
||||
runClient signKey tp h = do
|
||||
@@ -315,7 +351,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
CPResume -> withAdminRole $ hPutStrLn h "resume not implemented"
|
||||
CPClients -> withAdminRole $ do
|
||||
active <- unliftIO u (asks clients) >>= readTVarIO
|
||||
hPutStrLn h $ "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions"
|
||||
hPutStrLn h "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions"
|
||||
forM_ (IM.toList active) $ \(cid, Client {sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, subscriptions}) -> do
|
||||
connected' <- bshow <$> readTVarIO connected
|
||||
rcvActiveAt' <- strEncode <$> readTVarIO rcvActiveAt
|
||||
@@ -325,7 +361,13 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
subscriptions' <- bshow . M.size <$> readTVarIO subscriptions
|
||||
hPutStrLn h . B.unpack $ B.intercalate "," [bshow cid, encode sessionId, connected', strEncode createdAt, rcvActiveAt', sndActiveAt', bshow age, subscriptions']
|
||||
CPStats -> withAdminRole $ do
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgSentNtf, msgRecvNtf, qCount, msgCount} <- unliftIO u $ asks serverStats
|
||||
ss <- unliftIO u $ asks serverStats
|
||||
let putStat :: Show a => ByteString -> (ServerStats -> TVar a) -> IO ()
|
||||
putStat label var = readTVarIO (var ss) >>= \v -> B.hPutStr h $ label <> ": " <> bshow v <> "\n"
|
||||
putProxyStat :: ByteString -> (ServerStats -> ProxyStats) -> IO ()
|
||||
putProxyStat label var = do
|
||||
ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} <- atomically $ getProxyStatsData $ var ss
|
||||
B.hPutStr h $ label <> ": requests=" <> bshow _pRequests <> ", successes=" <> bshow _pSuccesses <> ", errorsConnect=" <> bshow _pErrorsConnect <> ", errorsCompat=" <> bshow _pErrorsCompat <> ", errorsOther=" <> bshow _pErrorsOther <> "\n"
|
||||
putStat "fromTime" fromTime
|
||||
putStat "qCreated" qCreated
|
||||
putStat "qSecured" qSecured
|
||||
@@ -338,9 +380,11 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
putStat "msgRecvNtf" msgRecvNtf
|
||||
putStat "qCount" qCount
|
||||
putStat "msgCount" msgCount
|
||||
where
|
||||
putStat :: Show a => String -> TVar a -> IO ()
|
||||
putStat label var = readTVarIO var >>= \v -> hPutStrLn h $ label <> ": " <> show v
|
||||
putProxyStat "pRelays" pRelays
|
||||
putProxyStat "pRelaysOwn" pRelaysOwn
|
||||
putProxyStat "pMsgFwds" pMsgFwds
|
||||
putProxyStat "pMsgFwdsOwn" pMsgFwdsOwn
|
||||
putStat "pMsgFwdsRecv" pMsgFwdsRecv
|
||||
CPStatsRTS -> getRTSStats >>= hPrint h
|
||||
CPThreads -> withAdminRole $ do
|
||||
#if MIN_VERSION_base(4,18,0)
|
||||
@@ -411,7 +455,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
hPutStrLn h "AUTH"
|
||||
|
||||
runClientTransport :: Transport c => THandleSMP c 'TServer -> M ()
|
||||
runClientTransport h@THandle {params = THandleParams {thVersion, sessionId}} = do
|
||||
runClientTransport h@THandle {params = thParams@THandleParams {thVersion, sessionId}} = do
|
||||
q <- asks $ tbqSize . config
|
||||
ts <- liftIO getSystemTime
|
||||
active <- asks clients
|
||||
@@ -424,7 +468,7 @@ runClientTransport h@THandle {params = THandleParams {thVersion, sessionId}} = d
|
||||
expCfg <- asks $ inactiveClientExpiration . config
|
||||
th <- newMVar h -- put TH under a fair lock to interleave messages and command responses
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId
|
||||
raceAny_ ([liftIO $ send th c, liftIO $ sendMsg th c, client c s, receive h c] <> disconnectThread_ c expCfg)
|
||||
raceAny_ ([liftIO $ send th c, liftIO $ sendMsg th c, client thParams c s, receive h c] <> disconnectThread_ c expCfg)
|
||||
`finally` clientDisconnected c
|
||||
where
|
||||
disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport h (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)]
|
||||
@@ -465,19 +509,19 @@ receive h@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiv
|
||||
forever $ do
|
||||
ts <- L.toList <$> liftIO (tGet h)
|
||||
atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime
|
||||
as <- partitionEithers <$> mapM cmdAction ts
|
||||
write sndQ $ fst as
|
||||
write rcvQ $ snd as
|
||||
(errs, cmds) <- partitionEithers <$> mapM cmdAction ts
|
||||
write sndQ errs
|
||||
write rcvQ cmds
|
||||
where
|
||||
cmdAction :: SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd))
|
||||
cmdAction (tAuth, authorized, (corrId, queueId, cmdOrError)) =
|
||||
cmdAction (tAuth, authorized, (corrId, entId, cmdOrError)) =
|
||||
case cmdOrError of
|
||||
Left e -> pure $ Left (corrId, queueId, ERR e)
|
||||
Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized queueId cmd
|
||||
Left e -> pure $ Left (corrId, entId, ERR e)
|
||||
Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized entId cmd
|
||||
where
|
||||
verified = \case
|
||||
VRVerified qr -> Right (qr, (corrId, queueId, cmd))
|
||||
VRFailed -> Left (corrId, queueId, ERR AUTH)
|
||||
VRVerified qr -> Right (qr, (corrId, entId, cmd))
|
||||
VRFailed -> Left (corrId, entId, ERR AUTH)
|
||||
write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty
|
||||
|
||||
send :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO ()
|
||||
@@ -545,16 +589,18 @@ verifyTransmission auth_ tAuth authorized queueId cmd =
|
||||
-- SEND will be accepted without authorization before the queue is secured with KEY command
|
||||
Cmd SSender SEND {} -> verifyQueue (\q -> Just q `verified` maybe (isNothing tAuth) verify (senderKey q)) <$> get SSender
|
||||
Cmd SSender PING -> pure $ VRVerified Nothing
|
||||
Cmd SSender RFWD {} -> pure $ VRVerified Nothing
|
||||
-- NSUB will not be accepted without authorization
|
||||
Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (Just q `verifiedWith`) (notifierKey <$> notifier q)) <$> get SNotifier
|
||||
Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (\n -> Just q `verifiedWith` notifierKey n) (notifier q)) <$> get SNotifier
|
||||
Cmd SProxiedClient _ -> pure $ VRVerified Nothing
|
||||
where
|
||||
verify = verifyCmdAuthorization auth_ tAuth authorized
|
||||
dummyVerify = verify (dummyAuthKey tAuth) `seq` VRFailed
|
||||
verifyQueue :: (QueueRec -> VerificationResult) -> Either ErrorType QueueRec -> VerificationResult
|
||||
verifyQueue = either (\_ -> dummyVerify)
|
||||
verifyQueue = either (const dummyVerify)
|
||||
verified q cond = if cond then VRVerified q else VRFailed
|
||||
verifiedWith q k = q `verified` verify k
|
||||
get :: SParty p -> M (Either ErrorType QueueRec)
|
||||
get :: DirectParty p => SParty p -> M (Either ErrorType QueueRec)
|
||||
get party = do
|
||||
st <- asks queueStore
|
||||
atomically $ getQueue st party queueId
|
||||
@@ -604,25 +650,88 @@ dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/Xo
|
||||
dummyKeyX25519 :: C.PublicKey 'C.X25519
|
||||
dummyKeyX25519 = "MCowBQYDK2VuAyEA4JGSMYht18H4mas/jHeBwfcM7jLwNYJNOAhi2/g4RXg="
|
||||
|
||||
client :: Client -> Server -> M ()
|
||||
client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do
|
||||
client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M ()
|
||||
client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands"
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= mapM processCommand
|
||||
>>= atomically . writeTBQueue sndQ
|
||||
forever $ do
|
||||
(proxied, rs) <- partitionEithers . L.toList <$> (mapM processCommand =<< atomically (readTBQueue rcvQ))
|
||||
forM_ (L.nonEmpty rs) reply
|
||||
-- TODO cancel this thread if the client gets disconnected
|
||||
-- TODO limit client concurrency
|
||||
forM_ (L.nonEmpty proxied) $ \cmds -> forkIO $ mapConcurrently processProxiedCmd cmds >>= reply
|
||||
where
|
||||
processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Transmission BrokerMsg)
|
||||
reply :: MonadIO m => NonEmpty (Transmission BrokerMsg) -> m ()
|
||||
reply = atomically . writeTBQueue sndQ
|
||||
processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Transmission BrokerMsg)
|
||||
processProxiedCmd (corrId, sessId, command) = (corrId, sessId,) <$> case command of
|
||||
PRXY srv auth -> ifM allowProxy getRelay (pure $ ERR $ PROXY BASIC_AUTH)
|
||||
where
|
||||
allowProxy = do
|
||||
ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config
|
||||
pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth
|
||||
getRelay = do
|
||||
ServerStats {pRelays, pRelaysOwn} <- asks serverStats
|
||||
let inc = mkIncProxyStats pRelays pRelaysOwn
|
||||
ProxyAgent {smpAgent = a} <- asks proxyAgent
|
||||
liftIO (runExceptT (getSMPServerClient'' a srv) `catch` (pure . Left . PCEIOError)) >>= \case
|
||||
Right (own, smp) -> do
|
||||
inc own pRequests
|
||||
case proxyResp smp of
|
||||
r@PKEY {} -> r <$ inc own pSuccesses
|
||||
r -> r <$ inc own pErrorsCompat
|
||||
Left e -> do
|
||||
let own = isOwnServer a srv
|
||||
inc own pRequests
|
||||
inc own $ if temporaryClientError e then pErrorsConnect else pErrorsOther
|
||||
pure . ERR $ smpProxyError e
|
||||
where
|
||||
proxyResp smp =
|
||||
let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp
|
||||
in case compatibleVRange thServerVRange proxiedSMPRelayVRange of
|
||||
-- Cap the destination relay version range to prevent client version fingerprinting.
|
||||
-- See comment for proxiedSMPRelayVersion.
|
||||
Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of
|
||||
Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey
|
||||
Nothing -> ERR $ transportErr TENoServerAuth
|
||||
_ -> ERR $ transportErr TEVersion
|
||||
PFWD fwdV pubKey encBlock -> do
|
||||
ProxyAgent {smpAgent = a} <- asks proxyAgent
|
||||
ServerStats {pMsgFwds, pMsgFwdsOwn} <- asks serverStats
|
||||
let inc = mkIncProxyStats pMsgFwds pMsgFwdsOwn
|
||||
atomically (lookupSMPServerClient a sessId) >>= \case
|
||||
Just (own, smp) -> do
|
||||
inc own pRequests
|
||||
if
|
||||
| v >= sendingProxySMPVersion ->
|
||||
liftIO (runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case
|
||||
Right r -> PRES r <$ inc own pSuccesses
|
||||
Left e -> case e of
|
||||
PCEProtocolError {} -> ERR err <$ inc own pSuccesses
|
||||
_ -> ERR err <$ inc own pErrorsOther
|
||||
where
|
||||
err = smpProxyError e
|
||||
| otherwise -> ERR (transportErr TEVersion) <$ inc own pErrorsCompat
|
||||
where
|
||||
THandleParams {thVersion = v} = thParams smp
|
||||
Nothing -> inc False pRequests >> inc False pErrorsConnect $> ERR (PROXY NO_SESSION)
|
||||
transportErr :: TransportError -> ErrorType
|
||||
transportErr = PROXY . BROKER . TRANSPORT
|
||||
mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> TVar Int) -> m ()
|
||||
mkIncProxyStats ps psOwn = \own sel -> do
|
||||
atomically $ modifyTVar' (sel ps) (+ 1)
|
||||
when own $ atomically $ modifyTVar' (sel psOwn) (+ 1)
|
||||
processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Either (Transmission (Command 'ProxiedClient)) (Transmission BrokerMsg))
|
||||
processCommand (qr_, (corrId, queueId, cmd)) = do
|
||||
st <- asks queueStore
|
||||
case cmd of
|
||||
Cmd SSender command ->
|
||||
case command of
|
||||
SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody
|
||||
PING -> pure (corrId, "", PONG)
|
||||
Cmd SNotifier NSUB -> subscribeNotifications
|
||||
Cmd SProxiedClient command -> pure $ Left (corrId, queueId, command)
|
||||
Cmd SSender command -> Right <$> case command of
|
||||
SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody
|
||||
PING -> pure (corrId, "", PONG)
|
||||
RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock
|
||||
Cmd SNotifier NSUB -> Right <$> subscribeNotifications
|
||||
Cmd SRecipient command ->
|
||||
case command of
|
||||
Right <$> case command of
|
||||
NEW rKey dhKey auth subMode ->
|
||||
ifM
|
||||
allowNew
|
||||
@@ -828,7 +937,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
|
||||
sendMessage :: QueueRec -> MsgFlags -> MsgBody -> M (Transmission BrokerMsg)
|
||||
sendMessage qr msgFlags msgBody
|
||||
| B.length msgBody > maxMessageLength = pure $ err LARGE_MSG
|
||||
| B.length msgBody > maxMessageLength thVersion = pure $ err LARGE_MSG
|
||||
| otherwise = case status qr of
|
||||
QueueOff -> return $ err AUTH
|
||||
QueueActive ->
|
||||
@@ -852,6 +961,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
atomically $ updatePeriodStats (activeQueues stats) (recipientId qr)
|
||||
pure ok
|
||||
where
|
||||
THandleParams {thVersion} = thParams'
|
||||
mkMessage :: C.MaxLenBS MaxMessageLen -> M Message
|
||||
mkMessage body = do
|
||||
msgId <- randomId =<< asks (msgIdBytes . config)
|
||||
@@ -886,6 +996,58 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
encNMsgMeta = C.cbEncrypt rcvNtfDhSecret cbNonce (smpEncode msgMeta) 128
|
||||
pure . (cbNonce,) $ fromRight "" encNMsgMeta
|
||||
|
||||
processForwardedCommand :: EncFwdTransmission -> M BrokerMsg
|
||||
processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do
|
||||
THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE $ transportErr TENoServerAuth) pure (thAuth thParams')
|
||||
sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret'
|
||||
let proxyNonce = C.cbNonce $ bs corrId
|
||||
s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s
|
||||
FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s'
|
||||
let clientSecret = C.dh' fwdKey serverPrivKey
|
||||
clientNonce = C.cbNonce $ bs fwdCorrId
|
||||
b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et
|
||||
let clntTHParams = smpTHParamsSetVersion fwdVersion thParams'
|
||||
-- only allowing single forwarded transactions
|
||||
t' <- case tParse clntTHParams b of
|
||||
t :| [] -> pure $ tDecodeParseValidate clntTHParams t
|
||||
_ -> throwE BLOCK
|
||||
let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret}
|
||||
-- process forwarded SEND
|
||||
r <-
|
||||
lift (rejectOrVerify clntThAuth t') >>= \case
|
||||
Left r -> pure r
|
||||
Right t''@(_, (corrId', entId', cmd')) -> case cmd' of
|
||||
Cmd SSender SEND {} ->
|
||||
-- Left will not be returned by processCommand, as only SEND command is allowed
|
||||
fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'')
|
||||
_ ->
|
||||
pure (corrId', entId', ERR $ CMD PROHIBITED)
|
||||
-- encode response
|
||||
r' <- case batchTransmissions (batch clntTHParams) (blockSize clntTHParams) [Right (Nothing, encodeTransmission clntTHParams r)] of
|
||||
[] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right
|
||||
TBError _ _ : _ -> throwE BLOCK
|
||||
TBTransmission b' _ : _ -> pure b'
|
||||
TBTransmissions b' _ _ : _ -> pure b'
|
||||
-- encrypt to client
|
||||
r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength
|
||||
-- encrypt to proxy
|
||||
let fr = FwdResponse {fwdCorrId, fwdResponse = r2}
|
||||
r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr)
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (pMsgFwdsRecv stats) (+ 1)
|
||||
pure $ RRES r3
|
||||
where
|
||||
rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd))
|
||||
rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) =
|
||||
case cmdOrError of
|
||||
Left e -> pure $ Left (corrId', entId', ERR e)
|
||||
Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd'
|
||||
where
|
||||
verified = \case
|
||||
VRVerified qr -> Right (qr, (corrId', entId', cmd'))
|
||||
VRFailed -> Left (corrId', entId', ERR AUTH)
|
||||
Right _ -> pure $ Left (corrId', entId', ERR $ CMD PROHIBITED)
|
||||
|
||||
deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> M (Transmission BrokerMsg)
|
||||
deliverMessage name qr rId sub q msg_ = time (name <> " deliver") $ do
|
||||
readTVarIO sub >>= \case
|
||||
@@ -894,9 +1056,10 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
Just msg ->
|
||||
let encMsg = encryptMsg qr msg
|
||||
in atomically (setDelivered s msg) $> (corrId, rId, MSG encMsg)
|
||||
_ -> forkSub $> ok
|
||||
_ -> pure ok
|
||||
_ -> forkSub $> resp
|
||||
_ -> pure resp
|
||||
where
|
||||
resp = (corrId, rId, OK)
|
||||
forkSub :: M ()
|
||||
forkSub = do
|
||||
atomically . modifyTVar' sub $ \s -> s {subThread = SubPending}
|
||||
|
||||
@@ -22,6 +22,7 @@ import Network.Socket (ServiceName)
|
||||
import qualified Network.TLS as T
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgent, SMPClientAgentConfig, newSMPClientAgent)
|
||||
import Simplex.Messaging.Crypto (KeyHash (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
@@ -79,7 +80,9 @@ data ServerConfig = ServerConfig
|
||||
-- | TCP transport config
|
||||
transportConfig :: TransportServerConfig,
|
||||
-- | run listener on control port
|
||||
controlPort :: Maybe ServiceName
|
||||
controlPort :: Maybe ServiceName,
|
||||
smpAgentCfg :: SMPClientAgentConfig,
|
||||
allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth`
|
||||
}
|
||||
|
||||
defMsgExpirationDays :: Int64
|
||||
@@ -110,8 +113,9 @@ data Env = Env
|
||||
tlsServerParams :: T.ServerParams,
|
||||
serverStats :: ServerStats,
|
||||
sockets :: SocketState,
|
||||
clientSeq :: TVar Int,
|
||||
clients :: TVar (IntMap Client)
|
||||
clientSeq :: TVar ClientId,
|
||||
clients :: TVar (IntMap Client),
|
||||
proxyAgent :: ProxyAgent -- senders served on this proxy
|
||||
}
|
||||
|
||||
data Server = Server
|
||||
@@ -122,8 +126,14 @@ data Server = Server
|
||||
savingLock :: Lock
|
||||
}
|
||||
|
||||
newtype ProxyAgent = ProxyAgent
|
||||
{ smpAgent :: SMPClientAgent
|
||||
}
|
||||
|
||||
type ClientId = Int
|
||||
|
||||
data Client = Client
|
||||
{ clientId :: Int,
|
||||
{ clientId :: ClientId,
|
||||
subscriptions :: TMap RecipientId (TVar Sub),
|
||||
ntfSubscriptions :: TMap NotifierId (),
|
||||
rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)),
|
||||
@@ -155,7 +165,7 @@ newServer = do
|
||||
savingLock <- createLock
|
||||
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock}
|
||||
|
||||
newClient :: TVar Int -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client
|
||||
newClient :: TVar ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client
|
||||
newClient nextClientId qSize thVersion sessionId createdAt = do
|
||||
clientId <- stateTVar nextClientId $ \next -> (next, next + 1)
|
||||
subscriptions <- TM.empty
|
||||
@@ -176,7 +186,7 @@ newSubscription subThread = do
|
||||
return Sub {subThread, delivered}
|
||||
|
||||
newEnv :: ServerConfig -> IO Env
|
||||
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, transportConfig} = do
|
||||
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg, transportConfig} = do
|
||||
server <- atomically newServer
|
||||
queueStore <- atomically newQueueStore
|
||||
msgStore <- atomically newMsgStore
|
||||
@@ -189,7 +199,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile,
|
||||
sockets <- atomically newSocketState
|
||||
clientSeq <- newTVarIO 0
|
||||
clients <- newTVarIO mempty
|
||||
return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients}
|
||||
proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random
|
||||
return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent}
|
||||
where
|
||||
restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode)
|
||||
restoreQueues QueueStore {queues, senders, notifiers} f = do
|
||||
@@ -205,3 +216,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile,
|
||||
addNotifier q = case notifier q of
|
||||
Nothing -> id
|
||||
Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q)
|
||||
|
||||
newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent
|
||||
newSMPProxyAgent smpAgentCfg random = do
|
||||
smpAgent <- newSMPClientAgent smpAgentCfg random
|
||||
pure ProxyAgent {smpAgent}
|
||||
|
||||
@@ -10,14 +10,18 @@ module Simplex.Messaging.Server.Main where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (void)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer)
|
||||
@@ -25,10 +29,11 @@ import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Version (mkVersionRange)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
import System.FilePath (combine)
|
||||
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
@@ -129,7 +134,7 @@ smpServerCLI cfgPath logPath =
|
||||
)
|
||||
<> "\n\n\
|
||||
\# control_port_admin_password:\n\
|
||||
\# control_port_user_password:\n\
|
||||
\# control_port_user_password:\n\n\
|
||||
\[TRANSPORT]\n\
|
||||
\# host is only used to print server address on start\n"
|
||||
<> ("host: " <> host <> "\n")
|
||||
@@ -137,6 +142,20 @@ smpServerCLI cfgPath logPath =
|
||||
<> "log_tls_errors: off\n\
|
||||
\websockets: off\n\
|
||||
\# control_port: 5224\n\n\
|
||||
\[PROXY]\n\
|
||||
\# Network configuration for SMP proxy client.\n\
|
||||
\# `host_mode` can be 'public' (default) or 'onion'.\n\
|
||||
\# It defines prefferred hostname for destination servers with multiple hostnames.\n\
|
||||
\# host_mode: public\n\
|
||||
\# required_host_mode: off\n\n\
|
||||
\# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics.\n\
|
||||
\# own_server_domains: \n\n\
|
||||
\# SOCKS proxy port for forwarding messages to destination servers.\n\
|
||||
\# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\
|
||||
\# socks_proxy: localhost:9050\n\n\
|
||||
\# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\
|
||||
\# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\
|
||||
\# socks_mode: onion\n\n\
|
||||
\[INACTIVE_CLIENTS]\n\
|
||||
\# TTL and interval to check inactive clients\n\
|
||||
\disconnect: off\n"
|
||||
@@ -214,8 +233,38 @@ smpServerCLI cfgPath logPath =
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
|
||||
alpn = Just supportedSMPHandshakes
|
||||
},
|
||||
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini
|
||||
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini,
|
||||
smpAgentCfg =
|
||||
defaultSMPClientAgentConfig
|
||||
{ smpCfg =
|
||||
(smpCfg defaultSMPClientAgentConfig)
|
||||
{ serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion,
|
||||
agreeSecret = True,
|
||||
networkConfig =
|
||||
defaultNetworkConfig
|
||||
{ socksProxy = either error id <$> strDecodeIni "PROXY" "socks_proxy" ini,
|
||||
socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini,
|
||||
hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini,
|
||||
requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini
|
||||
}
|
||||
},
|
||||
ownServerDomains = either (const []) textToOwnServers $ lookupValue "PROXY" "own_server_domains" ini,
|
||||
persistErrorInterval = 30 -- seconds
|
||||
},
|
||||
allowSMPProxy = True
|
||||
}
|
||||
textToSocksMode :: Text -> SocksMode
|
||||
textToSocksMode = \case
|
||||
"always" -> SMAlways
|
||||
"onion" -> SMOnion
|
||||
s -> error . T.unpack $ "Invalid socks_mode: " <> s
|
||||
textToHostMode :: Text -> HostMode
|
||||
textToHostMode = \case
|
||||
"public" -> HMPublic
|
||||
"onion" -> HMOnionViaSocks
|
||||
s -> error . T.unpack $ "Invalid host_mode: " <> s
|
||||
textToOwnServers :: Text -> [ByteString]
|
||||
textToOwnServers = map encodeUtf8 . T.words
|
||||
|
||||
data CliCommand
|
||||
= Init InitOptions
|
||||
@@ -306,4 +355,3 @@ cliCommandP cfgPath logPath iniFile =
|
||||
pure InitOptions {enableStoreLog, logStats, signAlgorithm, ip, fqdn, password, scripted}
|
||||
parseBasicAuth :: ReadM ServerPassword
|
||||
parseBasicAuth = eitherReader $ fmap ServerPassword . strDecode . B.pack
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId =
|
||||
where
|
||||
hasId = (||) <$> TM.member rId queues <*> TM.member sId senders
|
||||
|
||||
getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
|
||||
getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
|
||||
getQueue QueueStore {queues, senders, notifiers} party qId =
|
||||
toResult <$> (mapM readTVar =<< getVar)
|
||||
where
|
||||
|
||||
@@ -33,6 +33,11 @@ data ServerStats = ServerStats
|
||||
msgSentNtf :: TVar Int,
|
||||
msgRecvNtf :: TVar Int,
|
||||
activeQueuesNtf :: PeriodStats RecipientId,
|
||||
pRelays :: ProxyStats,
|
||||
pRelaysOwn :: ProxyStats,
|
||||
pMsgFwds :: ProxyStats,
|
||||
pMsgFwdsOwn :: ProxyStats,
|
||||
pMsgFwdsRecv :: TVar Int,
|
||||
qCount :: TVar Int,
|
||||
msgCount :: TVar Int
|
||||
}
|
||||
@@ -51,6 +56,11 @@ data ServerStatsData = ServerStatsData
|
||||
_msgSentNtf :: Int,
|
||||
_msgRecvNtf :: Int,
|
||||
_activeQueuesNtf :: PeriodStatsData RecipientId,
|
||||
_pRelays :: ProxyStatsData,
|
||||
_pRelaysOwn :: ProxyStatsData,
|
||||
_pMsgFwds :: ProxyStatsData,
|
||||
_pMsgFwdsOwn :: ProxyStatsData,
|
||||
_pMsgFwdsRecv :: Int,
|
||||
_qCount :: Int,
|
||||
_msgCount :: Int
|
||||
}
|
||||
@@ -71,9 +81,14 @@ newServerStats ts = do
|
||||
msgSentNtf <- newTVar 0
|
||||
msgRecvNtf <- newTVar 0
|
||||
activeQueuesNtf <- newPeriodStats
|
||||
pRelays <- newProxyStats
|
||||
pRelaysOwn <- newProxyStats
|
||||
pMsgFwds <- newProxyStats
|
||||
pMsgFwdsOwn <- newProxyStats
|
||||
pMsgFwdsRecv <- newTVar 0
|
||||
qCount <- newTVar 0
|
||||
msgCount <- newTVar 0
|
||||
pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount}
|
||||
pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv, qCount, msgCount}
|
||||
|
||||
getServerStatsData :: ServerStats -> STM ServerStatsData
|
||||
getServerStatsData s = do
|
||||
@@ -90,9 +105,14 @@ getServerStatsData s = do
|
||||
_msgSentNtf <- readTVar $ msgSentNtf s
|
||||
_msgRecvNtf <- readTVar $ msgRecvNtf s
|
||||
_activeQueuesNtf <- getPeriodStatsData $ activeQueuesNtf s
|
||||
_pRelays <- getProxyStatsData $ pRelays s
|
||||
_pRelaysOwn <- getProxyStatsData $ pRelaysOwn s
|
||||
_pMsgFwds <- getProxyStatsData $ pMsgFwds s
|
||||
_pMsgFwdsOwn <- getProxyStatsData $ pMsgFwdsOwn s
|
||||
_pMsgFwdsRecv <- readTVar $ pMsgFwdsRecv s
|
||||
_qCount <- readTVar $ qCount s
|
||||
_msgCount <- readTVar $ msgCount s
|
||||
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _qCount, _msgCount}
|
||||
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount}
|
||||
|
||||
setServerStats :: ServerStats -> ServerStatsData -> STM ()
|
||||
setServerStats s d = do
|
||||
@@ -109,28 +129,42 @@ setServerStats s d = do
|
||||
writeTVar (msgSentNtf s) $! _msgSentNtf d
|
||||
writeTVar (msgRecvNtf s) $! _msgRecvNtf d
|
||||
setPeriodStats (activeQueuesNtf s) (_activeQueuesNtf d)
|
||||
setProxyStats (pRelays s) $! _pRelays d
|
||||
setProxyStats (pRelaysOwn s) $! _pRelaysOwn d
|
||||
setProxyStats (pMsgFwds s) $! _pMsgFwds d
|
||||
setProxyStats (pMsgFwdsOwn s) $! _pMsgFwdsOwn d
|
||||
writeTVar (pMsgFwdsRecv s) $! _pMsgFwdsRecv d
|
||||
writeTVar (qCount s) $! _qCount d
|
||||
writeTVar (msgCount s) $! _msgCount d
|
||||
|
||||
instance StrEncoding ServerStatsData where
|
||||
strEncode ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount} =
|
||||
strEncode d =
|
||||
B.unlines
|
||||
[ "fromTime=" <> strEncode _fromTime,
|
||||
"qCreated=" <> strEncode _qCreated,
|
||||
"qSecured=" <> strEncode _qSecured,
|
||||
"qDeletedAll=" <> strEncode _qDeletedAll,
|
||||
"qDeletedNew=" <> strEncode _qDeletedNew,
|
||||
"qDeletedSecured=" <> strEncode _qDeletedSecured,
|
||||
"qCount=" <> strEncode _qCount,
|
||||
"msgSent=" <> strEncode _msgSent,
|
||||
"msgRecv=" <> strEncode _msgRecv,
|
||||
"msgExpired=" <> strEncode _msgExpired,
|
||||
"msgSentNtf=" <> strEncode _msgSentNtf,
|
||||
"msgRecvNtf=" <> strEncode _msgRecvNtf,
|
||||
[ "fromTime=" <> strEncode (_fromTime d),
|
||||
"qCreated=" <> strEncode (_qCreated d),
|
||||
"qSecured=" <> strEncode (_qSecured d),
|
||||
"qDeletedAll=" <> strEncode (_qDeletedAll d),
|
||||
"qDeletedNew=" <> strEncode (_qDeletedNew d),
|
||||
"qDeletedSecured=" <> strEncode (_qDeletedSecured d),
|
||||
"qCount=" <> strEncode (_qCount d),
|
||||
"msgSent=" <> strEncode (_msgSent d),
|
||||
"msgRecv=" <> strEncode (_msgRecv d),
|
||||
"msgExpired=" <> strEncode (_msgExpired d),
|
||||
"msgSentNtf=" <> strEncode (_msgSentNtf d),
|
||||
"msgRecvNtf=" <> strEncode (_msgRecvNtf d),
|
||||
"activeQueues:",
|
||||
strEncode _activeQueues,
|
||||
strEncode (_activeQueues d),
|
||||
"activeQueuesNtf:",
|
||||
strEncode _activeQueuesNtf
|
||||
strEncode (_activeQueuesNtf d),
|
||||
"pRelays:",
|
||||
strEncode (_pRelays d),
|
||||
"pRelaysOwn:",
|
||||
strEncode (_pRelaysOwn d),
|
||||
"pMsgFwds:",
|
||||
strEncode (_pMsgFwds d),
|
||||
"pMsgFwdsOwn:",
|
||||
strEncode (_pMsgFwdsOwn d),
|
||||
"pMsgFwdsRecv=" <> strEncode (_pMsgFwdsRecv d)
|
||||
]
|
||||
strP = do
|
||||
_fromTime <- "fromTime=" *> strP <* A.endOfLine
|
||||
@@ -157,7 +191,17 @@ instance StrEncoding ServerStatsData where
|
||||
optional ("activeQueuesNtf:" <* A.endOfLine) >>= \case
|
||||
Just _ -> strP <* optional A.endOfLine
|
||||
_ -> pure newPeriodStatsData
|
||||
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount = 0}
|
||||
_pRelays <- proxyStatsP "pRelays:"
|
||||
_pRelaysOwn <- proxyStatsP "pRelaysOwn:"
|
||||
_pMsgFwds <- proxyStatsP "pMsgFwds:"
|
||||
_pMsgFwdsOwn <- proxyStatsP "pMsgFwdsOwn:"
|
||||
_pMsgFwdsRecv <- "pMsgFwdsRecv=" *> strP <* A.endOfLine <|> pure 0
|
||||
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount = 0}
|
||||
where
|
||||
proxyStatsP key =
|
||||
optional (A.string key >> A.endOfLine) >>= \case
|
||||
Just _ -> strP <* optional A.endOfLine
|
||||
_ -> pure newProxyStatsData
|
||||
|
||||
data PeriodStats a = PeriodStats
|
||||
{ day :: TVar (Set a),
|
||||
@@ -231,3 +275,78 @@ updatePeriodStats stats pId = do
|
||||
updatePeriod month
|
||||
where
|
||||
updatePeriod pSel = modifyTVar' (pSel stats) (S.insert pId)
|
||||
|
||||
data ProxyStats = ProxyStats
|
||||
{ pRequests :: TVar Int,
|
||||
pSuccesses :: TVar Int, -- includes destination server error responses that will be forwarded to the client
|
||||
pErrorsConnect :: TVar Int,
|
||||
pErrorsCompat :: TVar Int,
|
||||
pErrorsOther :: TVar Int
|
||||
}
|
||||
|
||||
newProxyStats :: STM ProxyStats
|
||||
newProxyStats = do
|
||||
pRequests <- newTVar 0
|
||||
pSuccesses <- newTVar 0
|
||||
pErrorsConnect <- newTVar 0
|
||||
pErrorsCompat <- newTVar 0
|
||||
pErrorsOther <- newTVar 0
|
||||
pure ProxyStats {pRequests, pSuccesses, pErrorsConnect, pErrorsCompat, pErrorsOther}
|
||||
|
||||
data ProxyStatsData = ProxyStatsData
|
||||
{ _pRequests :: Int,
|
||||
_pSuccesses :: Int,
|
||||
_pErrorsConnect :: Int,
|
||||
_pErrorsCompat :: Int,
|
||||
_pErrorsOther :: Int
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newProxyStatsData :: ProxyStatsData
|
||||
newProxyStatsData = ProxyStatsData {_pRequests = 0, _pSuccesses = 0, _pErrorsConnect = 0, _pErrorsCompat = 0, _pErrorsOther = 0}
|
||||
|
||||
getProxyStatsData :: ProxyStats -> STM ProxyStatsData
|
||||
getProxyStatsData s = do
|
||||
_pRequests <- readTVar $ pRequests s
|
||||
_pSuccesses <- readTVar $ pSuccesses s
|
||||
_pErrorsConnect <- readTVar $ pErrorsConnect s
|
||||
_pErrorsCompat <- readTVar $ pErrorsCompat s
|
||||
_pErrorsOther <- readTVar $ pErrorsOther s
|
||||
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
|
||||
|
||||
getResetProxyStatsData :: ProxyStats -> STM ProxyStatsData
|
||||
getResetProxyStatsData s = do
|
||||
_pRequests <- swapTVar (pRequests s) 0
|
||||
_pSuccesses <- swapTVar (pSuccesses s) 0
|
||||
_pErrorsConnect <- swapTVar (pErrorsConnect s) 0
|
||||
_pErrorsCompat <- swapTVar (pErrorsCompat s) 0
|
||||
_pErrorsOther <- swapTVar (pErrorsOther s) 0
|
||||
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
|
||||
|
||||
setProxyStats :: ProxyStats -> ProxyStatsData -> STM ()
|
||||
setProxyStats s d = do
|
||||
writeTVar (pRequests s) $! _pRequests d
|
||||
writeTVar (pSuccesses s) $! _pSuccesses d
|
||||
writeTVar (pErrorsConnect s) $! _pErrorsConnect d
|
||||
writeTVar (pErrorsCompat s) $! _pErrorsCompat d
|
||||
writeTVar (pErrorsOther s) $! _pErrorsOther d
|
||||
|
||||
instance StrEncoding ProxyStatsData where
|
||||
strEncode ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} =
|
||||
"requests="
|
||||
<> strEncode _pRequests
|
||||
<> "\nsuccesses="
|
||||
<> strEncode _pSuccesses
|
||||
<> "\nerrorsConnect="
|
||||
<> strEncode _pErrorsConnect
|
||||
<> "\nerrorsCompat="
|
||||
<> strEncode _pErrorsCompat
|
||||
<> "\nerrorsOther="
|
||||
<> strEncode _pErrorsOther
|
||||
strP = do
|
||||
_pRequests <- "requests=" *> strP <* A.endOfLine
|
||||
_pSuccesses <- "successes=" *> strP <* A.endOfLine
|
||||
_pErrorsConnect <- "errorsConnect=" *> strP <* A.endOfLine
|
||||
_pErrorsCompat <- "errorsCompat=" *> strP <* A.endOfLine
|
||||
_pErrorsOther <- "errorsOther=" *> strP
|
||||
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Data.Composition ((.:.))
|
||||
import Data.Functor (($>))
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (($>>=))
|
||||
|
||||
data SessionVar a = SessionVar
|
||||
{ sessionVar :: TMVar a,
|
||||
@@ -36,3 +37,6 @@ removeSessVar' v sessKey vs =
|
||||
TM.lookup sessKey vs >>= \case
|
||||
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True
|
||||
_ -> pure False
|
||||
|
||||
tryReadSessVar :: Ord k => k -> TMap k (SessionVar a) -> STM (Maybe a)
|
||||
tryReadSessVar sessKey vs = TM.lookup sessKey vs $>>= (tryReadTMVar . sessionVar)
|
||||
|
||||
@@ -36,6 +36,7 @@ module Simplex.Messaging.Transport
|
||||
supportedSMPHandshakes,
|
||||
supportedClientSMPRelayVRange,
|
||||
supportedServerSMPRelayVRange,
|
||||
proxiedSMPRelayVRange,
|
||||
legacyServerSMPRelayVRange,
|
||||
currentClientSMPRelayVersion,
|
||||
legacyServerSMPRelayVersion,
|
||||
@@ -44,6 +45,7 @@ module Simplex.Messaging.Transport
|
||||
basicAuthSMPVersion,
|
||||
subModeSMPVersion,
|
||||
authCmdsSMPVersion,
|
||||
sendingProxySMPVersion,
|
||||
simplexMQVersion,
|
||||
smpBlockSize,
|
||||
TransportConfig (..),
|
||||
@@ -74,14 +76,13 @@ module Simplex.Messaging.Transport
|
||||
smpClientHandshake,
|
||||
tPutBlock,
|
||||
tGetBlock,
|
||||
serializeTransportError,
|
||||
transportErrorP,
|
||||
sendHandshake,
|
||||
getHandshake,
|
||||
smpTHParamsSetVersion,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Applicative (optional)
|
||||
import Control.Monad (forM)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except (throwE)
|
||||
@@ -127,7 +128,7 @@ smpBlockSize = 16384
|
||||
-- 4 - support command batching (7/17/2022)
|
||||
-- 5 - basic auth for SMP servers (11/12/2022)
|
||||
-- 6 - allow creating queues without subscribing (9/10/2023)
|
||||
-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (2/3/2024)
|
||||
-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (4/30/2024)
|
||||
|
||||
data SMPVersion
|
||||
|
||||
@@ -152,14 +153,25 @@ subModeSMPVersion = VersionSMP 6
|
||||
authCmdsSMPVersion :: VersionSMP
|
||||
authCmdsSMPVersion = VersionSMP 7
|
||||
|
||||
sendingProxySMPVersion :: VersionSMP
|
||||
sendingProxySMPVersion = VersionSMP 8
|
||||
|
||||
currentClientSMPRelayVersion :: VersionSMP
|
||||
currentClientSMPRelayVersion = VersionSMP 7
|
||||
currentClientSMPRelayVersion = VersionSMP 8
|
||||
|
||||
legacyServerSMPRelayVersion :: VersionSMP
|
||||
legacyServerSMPRelayVersion = VersionSMP 6
|
||||
|
||||
currentServerSMPRelayVersion :: VersionSMP
|
||||
currentServerSMPRelayVersion = VersionSMP 7
|
||||
currentServerSMPRelayVersion = VersionSMP 8
|
||||
|
||||
-- Max SMP protocol version to be used in e2e encrypted
|
||||
-- connection between client and server, as defined by SMP proxy.
|
||||
-- SMP proxy sets it to lower than its current version
|
||||
-- to prevent client version fingerprinting by the
|
||||
-- destination relays when clients upgrade at different times.
|
||||
proxiedSMPRelayVersion :: VersionSMP
|
||||
proxiedSMPRelayVersion = VersionSMP 8
|
||||
|
||||
-- minimal supported protocol version is 4
|
||||
-- TODO remove code that supports sending commands without batching
|
||||
@@ -172,6 +184,10 @@ legacyServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion legacyServerSMPR
|
||||
supportedServerSMPRelayVRange :: VersionRangeSMP
|
||||
supportedServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentServerSMPRelayVersion
|
||||
|
||||
-- This range initially allows only version 8 - see the comment above.
|
||||
proxiedSMPRelayVRange :: VersionRangeSMP
|
||||
proxiedSMPRelayVRange = mkVersionRange sendingProxySMPVersion proxiedSMPRelayVersion
|
||||
|
||||
supportedSMPHandshakes :: [ALPN]
|
||||
supportedSMPHandshakes = ["smp/1"]
|
||||
|
||||
@@ -338,6 +354,8 @@ type THandleSMP c p = THandle SMPVersion c p
|
||||
data THandleParams v p = THandleParams
|
||||
{ sessionId :: SessionId,
|
||||
blockSize :: Int,
|
||||
-- | server protocol version range
|
||||
thServerVRange :: VersionRange v,
|
||||
-- | agreed server protocol version
|
||||
thVersion :: Version v,
|
||||
-- | peer public key for command authorization and shared secrets for entity ID encryption
|
||||
@@ -370,6 +388,7 @@ data ServerHandshake = ServerHandshake
|
||||
{ smpVersionRange :: VersionRangeSMP,
|
||||
sessionId :: SessionId,
|
||||
-- pub key to agree shared secrets for command authorization and entity ID encryption.
|
||||
-- todo C.PublicKeyX25519
|
||||
authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey)
|
||||
}
|
||||
|
||||
@@ -421,6 +440,8 @@ authEncryptCmdsP v p = if v >= authCmdsSMPVersion then optional p else pure Noth
|
||||
data TransportError
|
||||
= -- | error parsing transport block
|
||||
TEBadBlock
|
||||
| -- | incompatible client or server version
|
||||
TEVersion
|
||||
| -- | message does not fit in transport block
|
||||
TELargeMsg
|
||||
| -- | incorrect session ID
|
||||
@@ -436,31 +457,29 @@ data TransportError
|
||||
data HandshakeError
|
||||
= -- | parsing error
|
||||
PARSE
|
||||
| -- | incompatible peer version
|
||||
VERSION
|
||||
| -- | incorrect server identity
|
||||
IDENTITY
|
||||
| -- | v7 authentication failed
|
||||
BAD_AUTH
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | SMP encrypted transport error parser.
|
||||
transportErrorP :: Parser TransportError
|
||||
transportErrorP =
|
||||
"BLOCK" $> TEBadBlock
|
||||
<|> "LARGE_MSG" $> TELargeMsg
|
||||
<|> "SESSION" $> TEBadSession
|
||||
<|> "NO_AUTH" $> TENoServerAuth
|
||||
<|> "HANDSHAKE " *> (TEHandshake <$> parseRead1)
|
||||
|
||||
-- | Serialize SMP encrypted transport error.
|
||||
serializeTransportError :: TransportError -> ByteString
|
||||
serializeTransportError = \case
|
||||
TEBadBlock -> "BLOCK"
|
||||
TELargeMsg -> "LARGE_MSG"
|
||||
TEBadSession -> "SESSION"
|
||||
TENoServerAuth -> "NO_AUTH"
|
||||
TEHandshake e -> "HANDSHAKE " <> bshow e
|
||||
instance Encoding TransportError where
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"BLOCK" -> pure TEBadBlock
|
||||
"VERSION" -> pure TEVersion
|
||||
"LARGE_MSG" -> pure TELargeMsg
|
||||
"SESSION" -> pure TEBadSession
|
||||
"NO_AUTH" -> pure TENoServerAuth
|
||||
"HANDSHAKE" -> TEHandshake <$> (A.space *> parseRead1)
|
||||
_ -> fail "bad TransportError"
|
||||
smpEncode = \case
|
||||
TEBadBlock -> "BLOCK"
|
||||
TEVersion -> "VERSION"
|
||||
TELargeMsg -> "LARGE_MSG"
|
||||
TEBadSession -> "SESSION"
|
||||
TENoServerAuth -> "NO_AUTH"
|
||||
TEHandshake e -> "HANDSHAKE " <> bshow e
|
||||
|
||||
-- | Pad and send block to SMP transport.
|
||||
tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ())
|
||||
@@ -490,9 +509,10 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do
|
||||
ClientHandshake {smpVersion = v, keyHash, authPubKey = k'}
|
||||
| keyHash /= kh ->
|
||||
throwE $ TEHandshake IDENTITY
|
||||
| v `isCompatible` smpVersionRange ->
|
||||
pure $ smpThHandleServer th v pk k'
|
||||
| otherwise -> throwE $ TEHandshake VERSION
|
||||
| otherwise ->
|
||||
case compatibleVRange' smpVersionRange v of
|
||||
Just (Compatible vr) -> pure $ smpTHandleServer th v vr pk k'
|
||||
Nothing -> throwE TEVersion
|
||||
|
||||
-- | Client SMP transport handshake.
|
||||
--
|
||||
@@ -503,8 +523,8 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do
|
||||
ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th
|
||||
if sessionId /= sessId
|
||||
then throwE TEBadSession
|
||||
else case smpVersionRange `compatibleVersion` smpVRange of
|
||||
Just (Compatible v) -> do
|
||||
else case smpVersionRange `compatibleVRange` smpVRange of
|
||||
Just (Compatible vr) -> do
|
||||
ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) ->
|
||||
liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
|
||||
case cert of
|
||||
@@ -513,26 +533,33 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do
|
||||
serverKey <- getServerVerifyKey c
|
||||
pubKey <- C.verifyX509 serverKey exact
|
||||
(,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
|
||||
let v = maxVersion vr
|
||||
sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_}
|
||||
pure $ smpThHandleClient th v (snd <$> ks_) ck_
|
||||
Nothing -> throwE $ TEHandshake VERSION
|
||||
pure $ smpTHandleClient th v vr (snd <$> ks_) ck_
|
||||
Nothing -> throwE TEVersion
|
||||
|
||||
smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer
|
||||
smpThHandleServer th v pk k_ =
|
||||
smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer
|
||||
smpTHandleServer th v vr pk k_ =
|
||||
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_}
|
||||
in smpThHandle_ th v (Just thAuth)
|
||||
in smpTHandle_ th v vr (Just thAuth)
|
||||
|
||||
smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient
|
||||
smpThHandleClient th v pk_ ck_ =
|
||||
smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient
|
||||
smpTHandleClient th v vr pk_ ck_ =
|
||||
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_
|
||||
in smpThHandle_ th v thAuth
|
||||
in smpTHandle_ th v vr thAuth
|
||||
|
||||
smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p
|
||||
smpThHandle_ th@THandle {params} v thAuth =
|
||||
smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p
|
||||
smpTHandle_ th@THandle {params} v vr thAuth =
|
||||
-- TODO drop SMP v6: make thAuth non-optional
|
||||
let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion}
|
||||
let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion}
|
||||
in (th :: THandleSMP c p) {params = params'}
|
||||
|
||||
-- This function is only used with v >= 8, so currently it's a simple record update.
|
||||
-- It may require some parameters update in the future, to be consistent with smpTHandle_.
|
||||
smpTHParamsSetVersion :: VersionSMP -> THandleParams SMPVersion p -> THandleParams SMPVersion p
|
||||
smpTHParamsSetVersion v params = params {thVersion = v}
|
||||
{-# INLINE smpTHParamsSetVersion #-}
|
||||
|
||||
sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO ()
|
||||
sendHandshake th = ExceptT . tPutBlock th . smpEncode
|
||||
|
||||
@@ -543,7 +570,17 @@ getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP
|
||||
smpTHandle :: Transport c => c -> THandleSMP c p
|
||||
smpTHandle c = THandle {connection = c, params}
|
||||
where
|
||||
params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True}
|
||||
v = VersionSMP 0
|
||||
params =
|
||||
THandleParams
|
||||
{ sessionId = tlsUnique c,
|
||||
blockSize = smpBlockSize,
|
||||
thServerVRange = versionToRange v,
|
||||
thVersion = v,
|
||||
thAuth = Nothing,
|
||||
implySessId = False,
|
||||
batch = True
|
||||
}
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''HandshakeError)
|
||||
|
||||
|
||||
@@ -152,12 +152,14 @@ timeoutThrow :: MonadUnliftIO m => e -> Int -> ExceptT e m a -> ExceptT e m a
|
||||
timeoutThrow e ms action = ExceptT (sequence <$> (ms `timeout` runExceptT action)) >>= maybe (throwError e) pure
|
||||
|
||||
threadDelay' :: Int64 -> IO ()
|
||||
threadDelay' time
|
||||
| time <= 0 = pure ()
|
||||
threadDelay' time = do
|
||||
let maxWait = min time $ fromIntegral (maxBound :: Int)
|
||||
threadDelay $ fromIntegral maxWait
|
||||
when (maxWait /= time) $ threadDelay' (time - maxWait)
|
||||
threadDelay' = loop
|
||||
where
|
||||
loop time
|
||||
| time <= 0 = pure ()
|
||||
| otherwise = do
|
||||
let maxWait = min time $ fromIntegral (maxBound :: Int)
|
||||
threadDelay $ fromIntegral maxWait
|
||||
loop $ time - maxWait
|
||||
|
||||
diffToMicroseconds :: NominalDiffTime -> Int64
|
||||
diffToMicroseconds diff = fromIntegral ((truncate $ diff * 1000000) :: Integer)
|
||||
|
||||
@@ -23,6 +23,8 @@ module Simplex.Messaging.Version
|
||||
isCompatibleRange,
|
||||
proveCompatible,
|
||||
compatibleVersion,
|
||||
compatibleVRange,
|
||||
compatibleVRange',
|
||||
)
|
||||
where
|
||||
|
||||
@@ -98,6 +100,7 @@ class VersionScope v => VersionI v a | a -> v where
|
||||
class VersionScope v => VersionRangeI v a | a -> v where
|
||||
type VersionT v a
|
||||
versionRange :: a -> VersionRange v
|
||||
toVersionRange :: a -> VersionRange v -> a
|
||||
toVersionT :: a -> Version v -> VersionT v a
|
||||
|
||||
instance VersionScope v => VersionI v (Version v) where
|
||||
@@ -108,6 +111,7 @@ instance VersionScope v => VersionI v (Version v) where
|
||||
instance VersionScope v => VersionRangeI v (VersionRange v) where
|
||||
type VersionT v (VersionRange v) = Version v
|
||||
versionRange = id
|
||||
toVersionRange _ vr = vr
|
||||
toVersionT _ v = v
|
||||
|
||||
newtype Compatible a = Compatible_ a
|
||||
@@ -135,5 +139,24 @@ compatibleVersion x vr =
|
||||
max1 = maxVersion $ versionRange x
|
||||
max2 = maxVersion vr
|
||||
|
||||
-- | intersection of version ranges
|
||||
compatibleVRange :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible a)
|
||||
compatibleVRange x vr =
|
||||
compatibleVRange_ x (max min1 min2) (min max1 max2)
|
||||
where
|
||||
VRange min1 max1 = versionRange x
|
||||
VRange min2 max2 = vr
|
||||
|
||||
-- | version range capped by compatible version
|
||||
compatibleVRange' :: VersionRangeI v a => a -> Version v -> Maybe (Compatible a)
|
||||
compatibleVRange' x v
|
||||
| v <= max1 = compatibleVRange_ x min1 v
|
||||
| otherwise = Nothing
|
||||
where
|
||||
VRange min1 max1 = versionRange x
|
||||
|
||||
compatibleVRange_ :: VersionRangeI v a => a -> Version v -> Version v -> Maybe (Compatible a)
|
||||
compatibleVRange_ x v1 v2 = Compatible_ . toVersionRange x <$> safeVersionRange v1 v2
|
||||
|
||||
mkCompatibleIf :: a -> Bool -> Maybe (Compatible a)
|
||||
x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing
|
||||
|
||||
+8
-8
@@ -13,7 +13,7 @@ module AgentTests (agentTests) where
|
||||
|
||||
import AgentTests.ConnectionRequestTests
|
||||
import AgentTests.DoubleRatchetTests (doubleRatchetTests)
|
||||
import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg')
|
||||
import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg', pattern SENT)
|
||||
import AgentTests.MigrationTests (migrationTests)
|
||||
import AgentTests.NotificationTests (notificationTests)
|
||||
import AgentTests.SQLiteTests (storeTests)
|
||||
@@ -27,7 +27,7 @@ import GHC.Stack (withFrozenCallStack)
|
||||
import Network.HTTP.Types (urlEncode)
|
||||
import SMPAgentClient
|
||||
import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CONF, INFO, MID, REQ)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CONF, INFO, MID, REQ, SENT)
|
||||
import qualified Simplex.Messaging.Agent.Protocol as A
|
||||
import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
@@ -242,7 +242,7 @@ testDuplexConnection' (alice, aPQ) (bob, bPQ) = do
|
||||
alice #: ("4a", "bob", "ACK 7") #> ("4a", "bob", OK)
|
||||
alice #: ("5", "bob", "OFF") #> ("5", "bob", OK)
|
||||
bob #: ("17", "alice", "SEND F 9\nmessage 3") #> ("17", "alice", A.MID 8 pq)
|
||||
bob <# ("", "alice", MERR 8 (SMP AUTH))
|
||||
bob <#= \case ("", "alice", MERR 8 (SMP _ AUTH)) -> True; _ -> False
|
||||
alice #: ("6", "bob", "DEL") #> ("6", "bob", OK)
|
||||
alice #:# "nothing else should be delivered to alice"
|
||||
|
||||
@@ -280,7 +280,7 @@ testDuplexConnRandomIds' (alice, aPQ) (bob, bPQ) = do
|
||||
alice #: ("4a", bobConn, "ACK 7") #> ("4a", bobConn, OK)
|
||||
alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK)
|
||||
bob #: ("17", aliceConn, "SEND F 9\nmessage 3") #> ("17", aliceConn, A.MID 8 pq)
|
||||
bob <# ("", aliceConn, MERR 8 (SMP AUTH))
|
||||
bob <#= \case ("", cId, MERR 8 (SMP _ AUTH)) -> cId == aliceConn; _ -> False
|
||||
alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK)
|
||||
alice #:# "nothing else should be delivered to alice"
|
||||
|
||||
@@ -383,7 +383,7 @@ testSubscrNotification t (server, _) client = do
|
||||
killThread server
|
||||
client <#. ("", "", DOWN testSMPServer ["conn1"])
|
||||
withSmpServer (ATransport t) $
|
||||
client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue
|
||||
client <#= \case ("", "conn1", ERR (SMP _ AUTH)) -> True; _ -> False -- this new server does not have the queue
|
||||
|
||||
testMsgDeliveryServerRestart :: forall c. Transport c => (c, InitialKeys) -> (c, PQSupport) -> IO ()
|
||||
testMsgDeliveryServerRestart (alice, aPQ) (bob, bPQ) = do
|
||||
@@ -429,8 +429,8 @@ testServerConnectionAfterError t _ = do
|
||||
bob #: ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT
|
||||
alice #: ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT
|
||||
withServer $ do
|
||||
alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False
|
||||
alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False
|
||||
alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False
|
||||
alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False
|
||||
bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False
|
||||
bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False
|
||||
bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK)
|
||||
@@ -630,7 +630,7 @@ syntaxTests t = do
|
||||
<> " subscribe "
|
||||
<> "14\nbob's connInfo"
|
||||
)
|
||||
>#> ("311", "a", "ERR SMP AUTH")
|
||||
>#> ("311", "a", "ERR SMP smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001 AUTH")
|
||||
describe "invalid" $ do
|
||||
it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX")
|
||||
where
|
||||
|
||||
@@ -45,12 +45,13 @@ module AgentTests.FunctionalAPITests
|
||||
pattern REQ,
|
||||
pattern Msg,
|
||||
pattern Msg',
|
||||
pattern SENT,
|
||||
agentCfgV7,
|
||||
)
|
||||
where
|
||||
|
||||
import AgentTests.ConnectionRequestTests (connReqData, queueAddr, testE2ERatchetParams12)
|
||||
import Control.Concurrent (killThread, threadDelay)
|
||||
import Control.Concurrent (forkIO, killThread, threadDelay)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
@@ -70,17 +71,16 @@ import Data.Word (Word16)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import GHC.Stack (withFrozenCallStack)
|
||||
import SMPAgentClient
|
||||
import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7)
|
||||
import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7)
|
||||
import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage)
|
||||
import qualified Simplex.Messaging.Agent as A
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT)
|
||||
import qualified Simplex.Messaging.Agent.Protocol as A
|
||||
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew))
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction')
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
@@ -171,6 +171,9 @@ pattern MsgErr msgId err msgBody <- MSG MsgMeta {recipient = (msgId, _), integri
|
||||
pattern MsgErr' :: AgentMsgId -> MsgErrorType -> PQEncryption -> MsgBody -> ACommand 'Agent 'AEConn
|
||||
pattern MsgErr' msgId err pq msgBody <- MSG MsgMeta {recipient = (msgId, _), integrity = MsgError err, pqEncryption = pq} _ msgBody
|
||||
|
||||
pattern SENT :: AgentMsgId -> ACommand 'Agent 'AEConn
|
||||
pattern SENT msgId = A.SENT msgId Nothing
|
||||
|
||||
pattern Rcvd :: AgentMsgId -> ACommand 'Agent 'AEConn
|
||||
pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMsgId, msgRcptStatus = MROk}]
|
||||
|
||||
@@ -422,7 +425,7 @@ functionalAPITests t = do
|
||||
describe "server with password" $ do
|
||||
let auth = Just "abcd"
|
||||
srv = ProtoServerWithAuth testSMPServer2
|
||||
authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP AUTH)
|
||||
authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP (B.unpack $ strEncode testSMPServer2) AUTH)
|
||||
it "should pass with correct password" $ testSMPServerConnectionTest t auth (srv auth) `shouldReturn` Nothing
|
||||
it "should fail without password" $ testSMPServerConnectionTest t auth (srv Nothing) `shouldReturn` authErr
|
||||
it "should fail with incorrect password" $ testSMPServerConnectionTest t auth (srv $ Just "wrong") `shouldReturn` authErr
|
||||
@@ -435,7 +438,8 @@ functionalAPITests t = do
|
||||
it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent t
|
||||
describe "user network info" $ do
|
||||
it "should wait for user network" testWaitForUserNetwork
|
||||
it "should not reset offline interval while offline" testDoNotResetOfflineInterval
|
||||
it "should not reset online to offline if happens too quickly" testDoNotResetOnlineToOffline
|
||||
it "should resume multiple threads" testResumeMultipleThreads
|
||||
|
||||
testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int
|
||||
testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do
|
||||
@@ -455,26 +459,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) =
|
||||
let v = basicAuthSMPVersion
|
||||
in allowNew && (isNothing srvAuth || (srvVersion >= v && clntVersion >= v && srvAuth == clntAuth))
|
||||
|
||||
testMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec
|
||||
testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec
|
||||
testMatrix2 t runTest = do
|
||||
it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn
|
||||
it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn
|
||||
it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn
|
||||
it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn
|
||||
it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn
|
||||
it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff
|
||||
it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff
|
||||
it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff
|
||||
it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True
|
||||
it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False
|
||||
it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False
|
||||
it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False
|
||||
it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False
|
||||
it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False
|
||||
it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False
|
||||
it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False
|
||||
it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False
|
||||
|
||||
testRatchetMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec
|
||||
testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec
|
||||
testRatchetMatrix2 t runTest = do
|
||||
it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn
|
||||
it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn
|
||||
it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn
|
||||
it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn
|
||||
it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff
|
||||
it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff
|
||||
it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff
|
||||
it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True
|
||||
it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False
|
||||
it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False
|
||||
it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False
|
||||
it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False
|
||||
it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff False
|
||||
it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff False
|
||||
it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff False
|
||||
|
||||
testServerMatrix2 :: ATransport -> (InitialAgentServers -> IO ()) -> Spec
|
||||
testServerMatrix2 t runTest = do
|
||||
@@ -482,10 +488,14 @@ testServerMatrix2 t runTest = do
|
||||
it "2 servers" $ withSmpServer t . withSmpServerOn t testPort2 $ runTest initAgentServers2
|
||||
|
||||
runTestCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO ()
|
||||
runTestCfg2 aCfg bCfg baseMsgId runTest =
|
||||
withAgentClientsCfg2 aCfg bCfg $ \a b -> runTest a b baseMsgId
|
||||
runTestCfg2 aCfg bCfg = runTestCfgServers2 aCfg bCfg initAgentServers
|
||||
{-# INLINE runTestCfg2 #-}
|
||||
|
||||
runTestCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO ()
|
||||
runTestCfgServers2 aCfg bCfg servers baseMsgId runTest =
|
||||
withAgentClientsCfgServers2 aCfg bCfg servers $ \a b -> runTest a b baseMsgId
|
||||
{-# INLINE runTestCfgServers2 #-}
|
||||
|
||||
withAgentClientsCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO ()
|
||||
withAgentClientsCfgServers2 aCfg bCfg servers runTest =
|
||||
withAgent 1 aCfg servers testDB $ \a ->
|
||||
@@ -506,8 +516,8 @@ withAgentClients3 runTest =
|
||||
withAgent 3 agentCfg initAgentServers testDB3 $ \c ->
|
||||
runTest a b c
|
||||
|
||||
runAgentClientTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()
|
||||
runAgentClientTest pqSupport alice@AgentClient {} bob baseId =
|
||||
runAgentClientTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()
|
||||
runAgentClientTest pqSupport viaProxy alice@AgentClient {} bob baseId =
|
||||
runRight_ $ do
|
||||
(bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqSupport) SMSubscribe
|
||||
aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" pqSupport SMSubscribe
|
||||
@@ -519,25 +529,26 @@ runAgentClientTest pqSupport alice@AgentClient {} bob baseId =
|
||||
get bob ##> ("", aliceId, A.INFO pqSupport "alice's connInfo")
|
||||
get bob ##> ("", aliceId, A.CON pqEnc)
|
||||
-- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4
|
||||
let proxySrv = if viaProxy then Just testSMPServer else Nothing
|
||||
1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 1)
|
||||
get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv)
|
||||
2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 2)
|
||||
get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv)
|
||||
get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False
|
||||
ackMessage bob aliceId (baseId + 1) Nothing
|
||||
get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False
|
||||
ackMessage bob aliceId (baseId + 2) Nothing
|
||||
3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 3)
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv)
|
||||
4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 4)
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv)
|
||||
get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False
|
||||
ackMessage alice bobId (baseId + 3) Nothing
|
||||
get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False
|
||||
ackMessage alice bobId (baseId + 4) Nothing
|
||||
suspendConnection alice bobId
|
||||
5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2"
|
||||
get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH))
|
||||
get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False
|
||||
deleteConnection alice bobId
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
where
|
||||
@@ -633,8 +644,8 @@ testAgentClient3 =
|
||||
get c =##> \case ("", connId, Msg "c5") -> connId == aIdForC; _ -> False
|
||||
ackMessage c aIdForC 5 Nothing
|
||||
|
||||
runAgentClientContactTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()
|
||||
runAgentClientContactTest pqSupport alice bob baseId =
|
||||
runAgentClientContactTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()
|
||||
runAgentClientContactTest pqSupport viaProxy alice bob baseId =
|
||||
runRight_ $ do
|
||||
(_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqSupport) SMSubscribe
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True qInfo pqSupport
|
||||
@@ -651,25 +662,26 @@ runAgentClientContactTest pqSupport alice bob baseId =
|
||||
get alice ##> ("", bobId, A.CON pqEnc)
|
||||
get bob ##> ("", aliceId, A.CON pqEnc)
|
||||
-- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4
|
||||
let proxySrv = if viaProxy then Just testSMPServer else Nothing
|
||||
1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 1)
|
||||
get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv)
|
||||
2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?"
|
||||
get alice ##> ("", bobId, SENT $ baseId + 2)
|
||||
get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv)
|
||||
get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False
|
||||
ackMessage bob aliceId (baseId + 1) Nothing
|
||||
get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False
|
||||
ackMessage bob aliceId (baseId + 2) Nothing
|
||||
3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 3)
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv)
|
||||
4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 4)
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv)
|
||||
get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False
|
||||
ackMessage alice bobId (baseId + 3) Nothing
|
||||
get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False
|
||||
ackMessage alice bobId (baseId + 4) Nothing
|
||||
suspendConnection alice bobId
|
||||
5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2"
|
||||
get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH))
|
||||
get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False
|
||||
deleteConnection alice bobId
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
where
|
||||
@@ -1115,7 +1127,7 @@ testExpireMessageQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testP
|
||||
5 <- sendMessage a bId SMP.noMsgFlags "2"
|
||||
liftIO $ threadDelay 1000000
|
||||
6 <- sendMessage a bId SMP.noMsgFlags "3" -- this won't expire
|
||||
get a =##> \case ("", c, MERR 5 (SMP QUOTA)) -> bId == c; _ -> False
|
||||
get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False
|
||||
pure (aId, bId)
|
||||
withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do
|
||||
subscribeConnection b' aId
|
||||
@@ -1143,15 +1155,15 @@ testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1}
|
||||
7 <- sendMessage a bId SMP.noMsgFlags "4"
|
||||
liftIO $ threadDelay 1000000
|
||||
8 <- sendMessage a bId SMP.noMsgFlags "5" -- this won't expire
|
||||
get a =##> \case ("", c, MERR 5 (SMP QUOTA)) -> bId == c; _ -> False
|
||||
get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False
|
||||
get a >>= \case
|
||||
("", c, MERR 6 (SMP QUOTA)) -> do
|
||||
("", c, MERR 6 (SMP _ QUOTA)) -> do
|
||||
liftIO $ bId `shouldBe` c
|
||||
get a =##> \case ("", c', MERR 7 (SMP QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP QUOTA)) -> bId == c'; _ -> False
|
||||
("", c, MERRS [6] (SMP QUOTA)) -> do
|
||||
get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP _ QUOTA)) -> bId == c'; _ -> False
|
||||
("", c, MERRS [6] (SMP _ QUOTA)) -> do
|
||||
liftIO $ bId `shouldBe` c
|
||||
get a =##> \case ("", c', MERR 7 (SMP QUOTA)) -> bId == c'; _ -> False
|
||||
("", c, MERRS [6, 7] (SMP QUOTA)) -> liftIO $ bId `shouldBe` c
|
||||
get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; _ -> False
|
||||
("", c, MERRS [6, 7] (SMP _ QUOTA)) -> liftIO $ bId `shouldBe` c
|
||||
r -> error $ show r
|
||||
pure (aId, bId)
|
||||
withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do
|
||||
@@ -1402,10 +1414,10 @@ makeConnection = makeConnection_ PQSupportOn
|
||||
makeConnection_ :: PQSupport -> AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnection_ pqEnc alice bob = makeConnectionForUsers_ pqEnc alice 1 bob 1
|
||||
|
||||
makeConnectionForUsers :: AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnectionForUsers :: HasCallStack => AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn
|
||||
|
||||
makeConnectionForUsers_ :: PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnectionForUsers_ :: HasCallStack => PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnectionForUsers_ pqSupport alice aliceUserId bob bobUserId = do
|
||||
(bobId, qInfo) <- A.createConnection alice aliceUserId True SCMInvitation Nothing (CR.IKNoPQ pqSupport) SMSubscribe
|
||||
aliceId <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport
|
||||
@@ -1503,9 +1515,9 @@ testSuspendingAgentCompleteSending t = withAgentClients2 $ \a b -> do
|
||||
liftIO $ suspendAgent b 5000000
|
||||
|
||||
withSmpServerStoreLogOn t testPort $ \_ -> runRight_ @AgentErrorType $ do
|
||||
pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
pGet b =##> \case ("", c, APC _ (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
pGet b =##> \case ("", c, APC SAEConn (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
("", "", SUSPENDED) <- nGet b
|
||||
|
||||
pGet a =##> \case ("", c, APC _ (Msg "hello too")) -> c == bId; ("", "", APC _ UP {}) -> True; _ -> False
|
||||
@@ -1709,7 +1721,7 @@ testAcceptContactAsync =
|
||||
ackMessage alice bobId (baseId + 4) Nothing
|
||||
suspendConnection alice bobId
|
||||
5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2"
|
||||
get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH))
|
||||
get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False
|
||||
deleteConnection alice bobId
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
where
|
||||
@@ -1755,7 +1767,7 @@ testWaitDeliveryNoPending t = withAgentClients2 $ \alice bob ->
|
||||
get alice =##> \case ("", cId, DEL_CONN) -> cId == bobId; _ -> False
|
||||
|
||||
3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2"
|
||||
get bob ##> ("", aliceId, MERR (baseId + 3) (SMP AUTH))
|
||||
get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 3); _ -> False
|
||||
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
liftIO $ noMessages bob "nothing else should be delivered to bob"
|
||||
@@ -1850,8 +1862,8 @@ testWaitDeliveryAUTHErr t =
|
||||
liftIO $ noMessages bob "nothing else should be delivered to bob"
|
||||
|
||||
withSmpServerStoreLogOn t testPort $ \_ -> do
|
||||
get alice ##> ("", bobId, MERR (baseId + 3) (SMP AUTH))
|
||||
get alice ##> ("", bobId, MERR (baseId + 4) (SMP AUTH))
|
||||
get alice =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == bobId && mId == (baseId + 3); _ -> False
|
||||
get alice =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == bobId && mId == (baseId + 4); _ -> False
|
||||
get alice =##> \case ("", cId, DEL_CONN) -> cId == bobId; _ -> False
|
||||
|
||||
liftIO $ noMessages alice "nothing else should be delivered to alice"
|
||||
@@ -2422,11 +2434,11 @@ testCreateQueueAuth srvVersion clnt1 clnt2 = do
|
||||
b <- getClient 2 clnt2 testDB2
|
||||
r <- runRight $ do
|
||||
tryError (createConnection a 1 True SCMInvitation Nothing SMSubscribe) >>= \case
|
||||
Left (SMP AUTH) -> pure 0
|
||||
Left (SMP _ AUTH) -> pure 0
|
||||
Left e -> throwError e
|
||||
Right (bId, qInfo) ->
|
||||
tryError (joinConnection b 1 True qInfo "bob's connInfo" SMSubscribe) >>= \case
|
||||
Left (SMP AUTH) -> pure 1
|
||||
Left (SMP _ AUTH) -> pure 1
|
||||
Left e -> throwError e
|
||||
Right aId -> do
|
||||
("", _, CONF confId _ "bob's connInfo") <- get a
|
||||
@@ -2698,9 +2710,7 @@ testWaitForUserNetwork = do
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False
|
||||
networkDelay a 100000
|
||||
networkDelay a 150000
|
||||
networkDelay a 200000
|
||||
networkDelay a 200000
|
||||
networkDelay a 100000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNCellular True
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNCellular False
|
||||
@@ -2710,36 +2720,66 @@ testWaitForUserNetwork = do
|
||||
(networkDelay a 50000)
|
||||
noNetworkDelay a
|
||||
where
|
||||
aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 100000, increaseAfter = 0, maxInterval = 200000}}
|
||||
aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0}
|
||||
|
||||
testDoNotResetOfflineInterval :: IO ()
|
||||
testDoNotResetOfflineInterval = do
|
||||
testDoNotResetOnlineToOffline :: IO ()
|
||||
testDoNotResetOnlineToOffline = do
|
||||
a <- getSMPAgentClient' 1 aCfg initAgentServers testDB
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi False
|
||||
networkDelay a 100000
|
||||
networkDelay a 150000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNCellular False
|
||||
networkDelay a 200000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False
|
||||
networkDelay a 200000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNCellular True
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi False
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi True
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNCellular False
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi False -- ingnored
|
||||
noNetworkDelay a
|
||||
threadDelay 100000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi False
|
||||
networkDelay a 100000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False
|
||||
networkDelay a 100000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi True
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False -- ingnored
|
||||
noNetworkDelay a
|
||||
where
|
||||
aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 100000, increaseAfter = 0, maxInterval = 200000}}
|
||||
aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0.1}
|
||||
|
||||
testResumeMultipleThreads :: IO ()
|
||||
testResumeMultipleThreads = do
|
||||
a <- getSMPAgentClient' 1 aCfg initAgentServers testDB
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False
|
||||
vs <-
|
||||
replicateM 50000 $ do
|
||||
v <- newEmptyTMVarIO
|
||||
void . forkIO $ waitNetwork a >>= atomically . putTMVar v
|
||||
pure v
|
||||
threadDelay 1000000
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNCellular True
|
||||
ts <- mapM (atomically . readTMVar) vs
|
||||
-- print $ minimum ts
|
||||
-- print $ maximum ts
|
||||
-- print $ sum ts `div` fromIntegral (length ts)
|
||||
let average = sum ts `div` fromIntegral (length ts)
|
||||
average < 3000000 `shouldBe` True
|
||||
maximum ts < 4000000 `shouldBe` True
|
||||
where
|
||||
aCfg = agentCfg {userOfflineDelay = 0}
|
||||
|
||||
noNetworkDelay :: AgentClient -> IO ()
|
||||
noNetworkDelay a = (10000 >) <$> waitNetwork a `shouldReturn` True
|
||||
noNetworkDelay a = do
|
||||
d <- waitNetwork a
|
||||
unless (d < 10000) $ expectationFailure $ "expected no delay, d = " <> show d
|
||||
|
||||
networkDelay :: AgentClient -> Int64 -> IO ()
|
||||
networkDelay a d' = (\d -> d' < d && d < d' + 15000) <$> waitNetwork a `shouldReturn` True
|
||||
networkDelay a d' = do
|
||||
d <- waitNetwork a
|
||||
unless (d' < d && d < d' + 15000) $ expectationFailure $ "expected delay " <> show d' <> ", d = " <> show d
|
||||
|
||||
waitNetwork :: AgentClient -> IO Int64
|
||||
waitNetwork a = do
|
||||
t <- getCurrentTime
|
||||
waitForUserNetwork a `runReaderT` agentEnv a
|
||||
waitForUserNetwork a
|
||||
t' <- getCurrentTime
|
||||
pure $ diffToMicroseconds $ diffUTCTime t' t
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module AgentTests.NotificationTests where
|
||||
|
||||
@@ -17,10 +17,6 @@ import AgentTests.FunctionalAPITests
|
||||
createConnection,
|
||||
exchangeGreetingsMsgId,
|
||||
get,
|
||||
withAgent,
|
||||
withAgentClients2,
|
||||
withAgentClientsCfgServers2,
|
||||
withAgentClients3,
|
||||
joinConnection,
|
||||
makeConnection,
|
||||
nGet,
|
||||
@@ -29,13 +25,18 @@ import AgentTests.FunctionalAPITests
|
||||
sendMessage,
|
||||
switchComplete,
|
||||
testServerMatrix2,
|
||||
withAgent,
|
||||
withAgentClients2,
|
||||
withAgentClients3,
|
||||
withAgentClientsCfg2,
|
||||
withAgentClientsCfgServers2,
|
||||
(##>),
|
||||
(=##>),
|
||||
pattern CON,
|
||||
pattern CONF,
|
||||
pattern INFO,
|
||||
pattern Msg,
|
||||
pattern SENT,
|
||||
)
|
||||
import Control.Concurrent (ThreadId, killThread, threadDelay)
|
||||
import Control.Monad
|
||||
@@ -55,12 +56,12 @@ import SMPClient (cfg, cfgV7, testPort, testPort2, testStoreLogFile2, withSmpSer
|
||||
import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage)
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore')
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..))
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..))
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Notifications.Types (NtfToken (..))
|
||||
import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..))
|
||||
@@ -151,7 +152,8 @@ testNtfMatrix t runTest = do
|
||||
it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest
|
||||
it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest
|
||||
skip "this case cannot be supported - see RFC" $
|
||||
it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest
|
||||
it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $
|
||||
runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest
|
||||
-- servers can be migrated in any order
|
||||
it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest
|
||||
it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest
|
||||
@@ -243,7 +245,7 @@ testNtfTokenSecondRegistration APNSMockServer {apnsQ} =
|
||||
-- now the second token registration is verified
|
||||
verifyNtfToken a' tkn nonce' verification'
|
||||
-- the first registration is removed
|
||||
Left (NTF AUTH) <- tryE $ checkNtfToken a tkn
|
||||
Left (NTF _ AUTH) <- tryE $ checkNtfToken a tkn
|
||||
-- and the second is active
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
@@ -258,7 +260,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
pure ntfData
|
||||
-- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server
|
||||
-- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server
|
||||
threadDelay 1000000
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a' ->
|
||||
-- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
|
||||
@@ -266,7 +268,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
withNtfServer t . runRight_ $ do
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
Left (NTF AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification
|
||||
Left (NTF _ AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification' <- ntfData' .-> "verification"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
@@ -17,6 +17,7 @@ module AgentTests.SQLiteTests (storeTests) where
|
||||
|
||||
import AgentTests.EqInstances ()
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.MVar
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (SomeException)
|
||||
import Control.Monad (replicateM_)
|
||||
@@ -45,9 +46,9 @@ import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction')
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Protocol (SubscriptionMode (..), pattern VersionSMPC)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
@@ -88,7 +89,7 @@ removeStore db = do
|
||||
removeFile $ dbFilePath db
|
||||
where
|
||||
close :: SQLiteStore -> IO ()
|
||||
close st = mapM_ DB.close =<< atomically (tryTakeTMVar $ dbConnection st)
|
||||
close st = mapM_ DB.close =<< tryTakeMVar (dbConnection st)
|
||||
|
||||
storeTests :: Spec
|
||||
storeTests = do
|
||||
@@ -708,15 +709,15 @@ testGetNextRcvChunkToDownload st = do
|
||||
withTransaction st $ \db -> do
|
||||
Right Nothing <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
|
||||
Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing)
|
||||
Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1"
|
||||
Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing)
|
||||
Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
|
||||
Left e <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
show e `shouldContain` "ConversionFailed"
|
||||
DB.query_ db "SELECT rcv_file_id FROM rcv_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)]
|
||||
|
||||
Right (Just RcvFileChunk {rcvFileEntityId}) <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
Right (Just (RcvFileChunk {rcvFileEntityId}, _)) <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
rcvFileEntityId `shouldBe` fId2
|
||||
|
||||
testGetNextRcvFileToDecrypt :: SQLiteStore -> Expectation
|
||||
@@ -725,10 +726,10 @@ testGetNextRcvFileToDecrypt st = do
|
||||
withTransaction st $ \db -> do
|
||||
Right Nothing <- getNextRcvFileToDecrypt db 86400
|
||||
|
||||
Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing)
|
||||
Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 1"
|
||||
DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1"
|
||||
Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing)
|
||||
Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 2"
|
||||
|
||||
Left e <- getNextRcvFileToDecrypt db 86400
|
||||
|
||||
@@ -281,12 +281,12 @@ randomSUB_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionSM
|
||||
randomSUB_ a v sessId = do
|
||||
g <- C.newRandom
|
||||
rId <- atomically $ C.randomBytes 24 g
|
||||
corrId <- atomically $ CorrId <$> C.randomBytes 24 g
|
||||
nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g
|
||||
(rKey, rpKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
thAuth_ <- testTHandleAuth v g rKey
|
||||
let thParams = testTHandleParams v sessId
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, rId, Cmd SRecipient SUB)
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) corrId tForAuth
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, rId, Cmd SRecipient SUB)
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) nonce tForAuth
|
||||
|
||||
randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg)
|
||||
randomSUBCmd = randomSUBCmd_ C.SEd25519
|
||||
@@ -311,13 +311,13 @@ randomSEND_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionS
|
||||
randomSEND_ a v sessId len = do
|
||||
g <- C.newRandom
|
||||
sId <- atomically $ C.randomBytes 24 g
|
||||
corrId <- atomically $ CorrId <$> C.randomBytes 3 g
|
||||
nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g
|
||||
(sKey, spKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
thAuth_ <- testTHandleAuth v g sKey
|
||||
msg <- atomically $ C.randomBytes len g
|
||||
let thParams = testTHandleParams v sessId
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, sId, Cmd SSender $ SEND noMsgFlags msg)
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) corrId tForAuth
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, sId, Cmd SSender $ SEND noMsgFlags msg)
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) nonce tForAuth
|
||||
|
||||
testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient
|
||||
testTHandleParams v sessionId =
|
||||
@@ -325,6 +325,7 @@ testTHandleParams v sessionId =
|
||||
{ sessionId,
|
||||
blockSize = smpBlockSize,
|
||||
thVersion = v,
|
||||
thServerVRange = supportedServerSMPRelayVRange,
|
||||
thAuth = Nothing,
|
||||
implySessId = v >= authCmdsSMPVersion,
|
||||
batch = True
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module CoreTests.ProtocolErrorTests where
|
||||
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Agent.Protocol as Agent
|
||||
import Simplex.Messaging.Client (ProxyClientError (..))
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..))
|
||||
import Simplex.RemoteControl.Types (RCErrorType (..))
|
||||
import Test.Hspec
|
||||
@@ -26,17 +25,34 @@ import Test.QuickCheck
|
||||
protocolErrorTests :: Spec
|
||||
protocolErrorTests = modifyMaxSuccess (const 1000) $ do
|
||||
describe "errors parsing / serializing" $ do
|
||||
it "should parse SMP protocol errors" . property $ \(err :: ErrorType) ->
|
||||
it "should parse SMP protocol errors" . property . forAll possibleErrorType $ \err ->
|
||||
smpDecode (smpEncode err) == Right err
|
||||
it "should parse SMP agent errors" . property $ \(err :: AgentErrorType) ->
|
||||
errHasSpaces err
|
||||
|| strDecode (strEncode err) == Right err
|
||||
it "should parse SMP agent errors" . property . forAll possibleAgentErrorType $ \err ->
|
||||
strDecode (strEncode err) == Right err
|
||||
where
|
||||
errHasSpaces = \case
|
||||
BROKER srv (RESPONSE e) -> hasSpaces srv || hasSpaces e
|
||||
BROKER srv _ -> hasSpaces srv
|
||||
possibleErrorType :: Gen ErrorType
|
||||
possibleErrorType = arbitrary >>= \e -> if skipErrorType e then discard else pure e
|
||||
possibleAgentErrorType :: Gen AgentErrorType
|
||||
possibleAgentErrorType =
|
||||
arbitrary >>= \case
|
||||
BROKER srv _ | hasSpaces srv -> discard
|
||||
SMP srv e | hasSpaces srv || skipErrorType e -> discard
|
||||
NTF srv e | hasSpaces srv || skipErrorType e -> discard
|
||||
XFTP srv _ | hasSpaces srv -> discard
|
||||
Agent.PROXY pxy srv _ | hasSpaces pxy || hasSpaces srv -> discard
|
||||
Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard
|
||||
Agent.PROXY _ _ (ProxyUnexpectedResponse e) | hasUnicode e -> discard
|
||||
Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard
|
||||
ok -> pure ok
|
||||
hasSpaces :: String -> Bool
|
||||
hasSpaces = any (== ' ')
|
||||
hasUnicode :: String -> Bool
|
||||
hasUnicode = any (>= '\255')
|
||||
skipErrorType = \case
|
||||
SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e
|
||||
SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> hasUnicode s
|
||||
SMP.PROXY (SMP.BROKER (RESPONSE s)) -> hasUnicode s
|
||||
_ -> False
|
||||
hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s)
|
||||
|
||||
deriving instance Generic AgentErrorType
|
||||
|
||||
@@ -44,6 +60,8 @@ deriving instance Generic CommandErrorType
|
||||
|
||||
deriving instance Generic ConnectionErrorType
|
||||
|
||||
deriving instance Generic ProxyClientError
|
||||
|
||||
deriving instance Generic BrokerErrorType
|
||||
|
||||
deriving instance Generic SMPAgentError
|
||||
@@ -54,6 +72,8 @@ deriving instance Generic ErrorType
|
||||
|
||||
deriving instance Generic CommandError
|
||||
|
||||
deriving instance Generic SMP.ProxyError
|
||||
|
||||
deriving instance Generic TransportError
|
||||
|
||||
deriving instance Generic HandshakeError
|
||||
@@ -68,6 +88,8 @@ instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary ProxyClientError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU
|
||||
@@ -78,6 +100,8 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary CommandError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary SMP.ProxyError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary TransportError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
module CoreTests.VersionRangeTests where
|
||||
|
||||
import Data.Word (Word16)
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Simplex.Messaging.Version
|
||||
@@ -38,6 +39,28 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do
|
||||
(vr 1 3, vr 2 3) `compatible` Just (Version 3)
|
||||
(vr 1 3, vr 2 4) `compatible` Just (Version 3)
|
||||
(vr 1 2, vr 3 4) `compatible` Nothing
|
||||
it "should choose mutually compatible version range (range intersection)" $ do
|
||||
(vr 1 1, vr 1 1) `compatibleVR` Just (vr 1 1)
|
||||
(vr 1 1, vr 1 2) `compatibleVR` Just (vr 1 1)
|
||||
(vr 1 2, vr 1 2) `compatibleVR` Just (vr 1 2)
|
||||
(vr 1 2, vr 2 3) `compatibleVR` Just (vr 2 2)
|
||||
(vr 1 3, vr 2 3) `compatibleVR` Just (vr 2 3)
|
||||
(vr 1 3, vr 2 4) `compatibleVR` Just (vr 2 3)
|
||||
(vr 1 2, vr 3 4) `compatibleVR` Nothing
|
||||
it "should choose compatible version range with changed max version (capped range)" $ do
|
||||
(vr 1 1, 1) `compatibleVR'` Just (vr 1 1)
|
||||
(vr 1 1, 2) `compatibleVR'` Nothing
|
||||
(vr 1 2, 2) `compatibleVR'` Just (vr 1 2)
|
||||
(vr 1 2, 3) `compatibleVR'` Nothing
|
||||
(vr 1 3, 2) `compatibleVR'` Just (vr 1 2)
|
||||
(vr 1 3, 3) `compatibleVR'` Just (vr 1 3)
|
||||
(vr 1 3, 4) `compatibleVR'` Nothing
|
||||
(vr 2 3, 1) `compatibleVR'` Nothing
|
||||
(vr 2 3, 2) `compatibleVR'` Just (vr 2 2)
|
||||
(vr 2 3, 3) `compatibleVR'` Just (vr 2 3)
|
||||
(vr 2 4, 1) `compatibleVR'` Nothing
|
||||
(vr 2 4, 3) `compatibleVR'` Just (vr 2 3)
|
||||
(vr 2 4, 4) `compatibleVR'` Just (vr 2 4)
|
||||
it "should check if version is compatible" $ do
|
||||
isCompatible @T (Version 1) (vr 1 2) `shouldBe` True
|
||||
isCompatible @T (Version 2) (vr 1 2) `shouldBe` True
|
||||
@@ -63,3 +86,16 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do
|
||||
case compatibleVersion vr1 vr2 of
|
||||
Just (Compatible v') -> Just v' `shouldBe` v
|
||||
Nothing -> Nothing `shouldBe` v
|
||||
compatibleVR :: (VersionRange T, VersionRange T) -> Maybe (VersionRange T) -> Expectation
|
||||
(vr1, vr2) `compatibleVR` vr' = do
|
||||
(vr1, vr2) `checkCompatibleVR` vr'
|
||||
(vr2, vr1) `checkCompatibleVR` vr'
|
||||
(vr1, vr2) `checkCompatibleVR` vr' =
|
||||
case compatibleVRange vr1 vr2 of
|
||||
Just (Compatible vr'') -> Just vr'' `shouldBe` vr'
|
||||
Nothing -> Nothing `shouldBe` vr'
|
||||
compatibleVR' :: (VersionRange T, Word16) -> Maybe (VersionRange T) -> Expectation
|
||||
(vr1, v2) `compatibleVR'` vr' =
|
||||
case compatibleVRange' vr1 (Version v2) of
|
||||
Just (Compatible vr'') -> Just vr'' `shouldBe` vr'
|
||||
Nothing -> Nothing `shouldBe` vr'
|
||||
|
||||
+10
-2
@@ -20,7 +20,8 @@ import qualified Database.SQLite.Simple as SQL
|
||||
import Network.Socket (ServiceName)
|
||||
import NtfClient (ntfTestPort)
|
||||
import SMPClient
|
||||
( serverBracket,
|
||||
( proxyVRange,
|
||||
serverBracket,
|
||||
testKeyHash,
|
||||
testPort,
|
||||
testPort2,
|
||||
@@ -34,7 +35,7 @@ import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Server (runSMPAgentBlocking)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew))
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction')
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig)
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig)
|
||||
import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig)
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth)
|
||||
@@ -198,6 +199,10 @@ initAgentServers =
|
||||
initAgentServers2 :: InitialAgentServers
|
||||
initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]}
|
||||
|
||||
initAgentServersProxy :: SMPProxyMode -> SMPProxyFallback -> InitialAgentServers
|
||||
initAgentServersProxy smpProxyMode smpProxyFallback =
|
||||
initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode, smpProxyFallback}}
|
||||
|
||||
agentCfg :: AgentConfig
|
||||
agentCfg =
|
||||
defaultAgentConfig
|
||||
@@ -217,6 +222,9 @@ agentCfg =
|
||||
where
|
||||
networkConfig = defaultNetworkConfig {tcpConnectTimeout = 1_000_000, tcpTimeout = 2_000_000}
|
||||
|
||||
agentProxyCfg :: AgentConfig
|
||||
agentProxyCfg = agentCfg {smpCfg = (smpCfg agentCfg) {serverVRange = proxyVRange}}
|
||||
|
||||
fastRetryInterval :: RetryInterval
|
||||
fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000}
|
||||
|
||||
|
||||
+31
-3
@@ -16,7 +16,8 @@ import Control.Monad.Except (runExceptT)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Network.Socket
|
||||
import Simplex.Messaging.Client (chooseTransportHost, defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Protocol
|
||||
@@ -75,8 +76,12 @@ testSMPClient = testSMPClientVR supportedClientSMPRelayVRange
|
||||
testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a
|
||||
testSMPClientVR vr client = do
|
||||
Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost
|
||||
testSMPClient_ useHost testPort vr client
|
||||
|
||||
testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a
|
||||
testSMPClient_ host port vr client = do
|
||||
let tcConfig = defaultTransportClientConfig {Client.alpn = clientALPN}
|
||||
runTransportClient tcConfig Nothing useHost testPort (Just testKeyHash) $ \h ->
|
||||
runTransportClient tcConfig Nothing host port (Just testKeyHash) $ \h ->
|
||||
runExceptT (smpClientHandshake h Nothing testKeyHash vr) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
@@ -112,12 +117,32 @@ cfg =
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedSMPHandshakes},
|
||||
controlPort = Nothing
|
||||
controlPort = Nothing,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
allowSMPProxy = False
|
||||
}
|
||||
|
||||
cfgV7 :: ServerConfig
|
||||
cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}
|
||||
|
||||
cfgV8 :: ServerConfig
|
||||
cfgV8 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion}
|
||||
|
||||
proxyCfg :: ServerConfig
|
||||
proxyCfg =
|
||||
cfgV7
|
||||
{ allowSMPProxy = True,
|
||||
smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion,
|
||||
smpAgentCfg =
|
||||
defaultSMPClientAgentConfig
|
||||
{ smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = proxyVRange, agreeSecret = True},
|
||||
persistErrorInterval = 3 -- seconds
|
||||
}
|
||||
}
|
||||
|
||||
proxyVRange :: VersionRangeSMP
|
||||
proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion
|
||||
|
||||
withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile}
|
||||
|
||||
@@ -155,6 +180,9 @@ withSmpServer t = withSmpServerOn t testPort
|
||||
withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a
|
||||
withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const
|
||||
|
||||
withSmpServerProxy :: HasCallStack => ATransport -> IO a -> IO a
|
||||
withSmpServerProxy t = withSmpServerConfigOn t proxyCfg testPort . const
|
||||
|
||||
runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a
|
||||
runSmpTest test = withSmpServer (transport @c) $ testSMPClient test
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module SMPProxyTests where
|
||||
|
||||
import AgentTests.FunctionalAPITests
|
||||
import Control.Monad.Trans.Except (runExceptT)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import SMPAgentClient
|
||||
import SMPClient
|
||||
import ServerTests (decryptMsgV3, sendRecv)
|
||||
import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage)
|
||||
import qualified Simplex.Messaging.Agent as A
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..))
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ)
|
||||
import qualified Simplex.Messaging.Agent.Protocol as A
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..))
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Version (mkVersionRange)
|
||||
import Test.Hspec
|
||||
import UnliftIO
|
||||
|
||||
smpProxyTests :: Spec
|
||||
smpProxyTests = do
|
||||
describe "server configuration" $ do
|
||||
it "refuses proxy handshake unless enabled" testNoProxy
|
||||
it "checks basic auth in proxy requests" testProxyAuth
|
||||
describe "proxy requests" $ do
|
||||
describe "bad relay URIs" $ do
|
||||
xit "host not resolved" todo
|
||||
xit "when SMP port blackholed" todo
|
||||
xit "no SMP service at host/port" todo
|
||||
xit "bad SMP fingerprint" todo
|
||||
xit "batching proxy requests" todo
|
||||
describe "deliver message via SMP proxy" $ do
|
||||
let srv1 = SMPServer testHost testPort testKeyHash
|
||||
srv2 = SMPServer testHost testPort2 testKeyHash
|
||||
describe "client API" $ do
|
||||
let maxLen = maxMessageLength sendingProxySMPVersion
|
||||
describe "one server" $ do
|
||||
it "deliver via proxy" . oneServer $ do
|
||||
deliverMessageViaProxy srv1 srv1 C.SEd448 "hello 1" "hello 2"
|
||||
describe "two servers" $ do
|
||||
let proxyServ = srv1
|
||||
relayServ = srv2
|
||||
(msg1, msg2) <- runIO $ do
|
||||
g <- C.newRandom
|
||||
atomically $ (,) <$> C.randomBytes maxLen g <*> C.randomBytes maxLen g
|
||||
it "deliver via proxy" . twoServersFirstProxy $
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2"
|
||||
it "max message size, Ed448 keys" . twoServersFirstProxy $
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd448 msg1 msg2
|
||||
it "max message size, Ed25519 keys" . twoServersFirstProxy $
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg1 msg2
|
||||
it "max message size, X25519 keys" . twoServersFirstProxy $
|
||||
deliverMessageViaProxy proxyServ relayServ C.SX25519 msg1 msg2
|
||||
describe "agent API" $ do
|
||||
describe "one server" $ do
|
||||
it "always via proxy" . oneServer $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2"
|
||||
it "without proxy" . oneServer $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2"
|
||||
describe "two servers" $ do
|
||||
it "always via proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2"
|
||||
it "both via proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2"
|
||||
it "first via proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2"
|
||||
it "without proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2"
|
||||
it "first via proxy for unknown" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2"
|
||||
it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2"
|
||||
it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $
|
||||
agentViaProxyVersionError
|
||||
where
|
||||
oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg testPort . const
|
||||
twoServers = twoServers_ proxyCfg proxyCfg
|
||||
twoServersFirstProxy = twoServers_ proxyCfg cfgV8
|
||||
twoServers_ cfg1 cfg2 runTest =
|
||||
withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ ->
|
||||
withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest
|
||||
|
||||
deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO ()
|
||||
deliverMessageViaProxy proxyServ relayServ alg msg msg' = do
|
||||
g <- C.newRandom
|
||||
-- set up proxy
|
||||
Right pc <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ())
|
||||
THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc
|
||||
-- set up relay
|
||||
msgQ <- newTBQueueIO 4
|
||||
Right rc <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ())
|
||||
runRight_ $ do
|
||||
-- prepare receiving queue
|
||||
(rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g
|
||||
(rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe
|
||||
let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv
|
||||
-- get proxy session
|
||||
sess <- connectSMPProxiedRelay pc relayServ (Just "correct")
|
||||
-- send via proxy to unsecured queue
|
||||
Right () <- proxySMPMessage pc sess Nothing sndId noMsgFlags msg
|
||||
-- receive 1
|
||||
(_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody})))]) <- atomically $ readTBQueue msgQ
|
||||
liftIO $ dec msgId encBody `shouldBe` Right msg
|
||||
ackSMPMessage rc rPriv rcvId msgId
|
||||
-- secure queue
|
||||
(sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g
|
||||
secureSMPQueue rc rPriv rcvId sPub
|
||||
-- send via proxy to secured queue
|
||||
Right () <- proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg'
|
||||
-- receive 2
|
||||
(_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'})))]) <- atomically $ readTBQueue msgQ
|
||||
liftIO $ dec msgId' encBody' `shouldBe` Right msg'
|
||||
ackSMPMessage rc rPriv rcvId msgId'
|
||||
|
||||
agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> IO ()
|
||||
agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 =
|
||||
withAgent 1 aCfg (servers aTestCfg) testDB $ \alice ->
|
||||
withAgent 2 aCfg (servers bTestCfg) testDB2 $ \bob -> runRight_ $ do
|
||||
(bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe
|
||||
aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice
|
||||
liftIO $ pqSup' `shouldBe` PQSupportOn
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
let pqEnc = CR.PQEncOn
|
||||
get alice ##> ("", bobId, A.CON pqEnc)
|
||||
get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo")
|
||||
get bob ##> ("", aliceId, A.CON pqEnc)
|
||||
-- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4
|
||||
let aProxySrv = if aViaProxy then Just $ L.head aSrvs else Nothing
|
||||
1 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1
|
||||
get alice ##> ("", bobId, A.SENT (baseId + 1) aProxySrv)
|
||||
2 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg2
|
||||
get alice ##> ("", bobId, A.SENT (baseId + 2) aProxySrv)
|
||||
get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False
|
||||
ackMessage bob aliceId (baseId + 1) Nothing
|
||||
get bob =##> \case ("", c, Msg' _ pq msg2') -> c == aliceId && pq == pqEnc && msg2 == msg2'; _ -> False
|
||||
ackMessage bob aliceId (baseId + 2) Nothing
|
||||
let bProxySrv = if bViaProxy then Just $ L.head bSrvs else Nothing
|
||||
3 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg1
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 3) bProxySrv)
|
||||
4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv)
|
||||
get alice =##> \case ("", c, Msg' _ pq msg1') -> c == bobId && pq == pqEnc && msg1 == msg1'; _ -> False
|
||||
ackMessage alice bobId (baseId + 3) Nothing
|
||||
get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False
|
||||
ackMessage alice bobId (baseId + 4) Nothing
|
||||
where
|
||||
baseId = 3
|
||||
msgId = subtract baseId . fst
|
||||
aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg}
|
||||
servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs}
|
||||
|
||||
agentViaProxyVersionError :: IO ()
|
||||
agentViaProxyVersionError =
|
||||
withAgent 1 agentProxyCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do
|
||||
Left (A.BROKER _ (TRANSPORT TEVersion)) <-
|
||||
withAgent 2 agentProxyCfg (servers [SMPServer testHost testPort2 testKeyHash]) testDB2 $ \bob -> runExceptT $ do
|
||||
(_bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe
|
||||
A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
pure ()
|
||||
where
|
||||
servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs}
|
||||
|
||||
testNoProxy :: IO ()
|
||||
testNoProxy = do
|
||||
withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do
|
||||
testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do
|
||||
(_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing)
|
||||
reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH)
|
||||
|
||||
testProxyAuth :: IO ()
|
||||
testProxyAuth = do
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do
|
||||
testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do
|
||||
(_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong")
|
||||
reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH)
|
||||
where
|
||||
proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"}
|
||||
|
||||
todo :: IO ()
|
||||
todo = do
|
||||
fail "TODO"
|
||||
@@ -183,12 +183,12 @@ testCreateSecure (ATransport t) =
|
||||
Resp "dabc" _ err5 <- sendRecv s ("", "dabc", sId, _SEND "hello")
|
||||
(err5, ERR AUTH) #== "rejects unsigned SEND"
|
||||
|
||||
let maxAllowedMessage = B.replicate maxMessageLength '-'
|
||||
let maxAllowedMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion) '-'
|
||||
Resp "bcda" _ OK <- signSendRecv s sKey ("bcda", sId, _SEND maxAllowedMessage)
|
||||
Resp "" _ (Msg mId3 msg3) <- tGet1 r
|
||||
(dec mId3 msg3, Right maxAllowedMessage) #== "delivers message of max size"
|
||||
|
||||
let biggerMessage = B.replicate (maxMessageLength + 1) '-'
|
||||
let biggerMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion + 1) '-'
|
||||
Resp "bcda" _ (ERR LARGE_MSG) <- signSendRecv s sKey ("bcda", sId, _SEND biggerMessage)
|
||||
pure ()
|
||||
|
||||
@@ -608,7 +608,7 @@ testRestoreMessages at@(ATransport t) =
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 2
|
||||
logSize testStoreMsgsFile `shouldReturn` 5
|
||||
logSize testServerStatsBackupFile `shouldReturn` 20
|
||||
logSize testServerStatsBackupFile `shouldReturn` 45
|
||||
Right stats1 <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
checkStats stats1 [rId] 5 1
|
||||
|
||||
@@ -626,7 +626,7 @@ testRestoreMessages at@(ATransport t) =
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
-- the last message is not removed because it was not ACK'd
|
||||
logSize testStoreMsgsFile `shouldReturn` 3
|
||||
logSize testServerStatsBackupFile `shouldReturn` 20
|
||||
logSize testServerStatsBackupFile `shouldReturn` 45
|
||||
Right stats2 <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
checkStats stats2 [rId] 5 3
|
||||
|
||||
@@ -645,7 +645,7 @@ testRestoreMessages at@(ATransport t) =
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
logSize testStoreMsgsFile `shouldReturn` 0
|
||||
logSize testServerStatsBackupFile `shouldReturn` 20
|
||||
logSize testServerStatsBackupFile `shouldReturn` 45
|
||||
Right stats3 <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
checkStats stats3 [rId] 5 5
|
||||
|
||||
@@ -931,8 +931,6 @@ instance Eq C.ASignature where
|
||||
Just Refl -> s == s'
|
||||
_ -> False
|
||||
|
||||
deriving instance Eq (C.Signature a)
|
||||
|
||||
syntaxTests :: ATransport -> Spec
|
||||
syntaxTests (ATransport t) = do
|
||||
it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN)
|
||||
|
||||
@@ -21,6 +21,7 @@ import GHC.IO.Exception (IOException (..))
|
||||
import qualified GHC.IO.Exception as IOException
|
||||
import NtfServerTests (ntfServerTests)
|
||||
import RemoteControl (remoteControlTests)
|
||||
import SMPProxyTests (smpProxyTests)
|
||||
import ServerTests
|
||||
import Simplex.Messaging.Transport (TLS, Transport (..))
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
@@ -59,6 +60,7 @@ main = do
|
||||
describe "SMP server via WebSockets" $ serverTests (transport @WS)
|
||||
describe "Notifications server" $ ntfServerTests (transport @TLS)
|
||||
describe "SMP client agent" $ agentTests (transport @TLS)
|
||||
describe "SMP proxy" smpProxyTests
|
||||
describe "XFTP" $ do
|
||||
describe "XFTP server" xftpServerTests
|
||||
describe "XFTP file description" fileDescriptionTests
|
||||
|
||||
+21
-16
@@ -69,7 +69,7 @@ xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do
|
||||
describe "server with password" $ do
|
||||
let auth = Just "abcd"
|
||||
srv = ProtoServerWithAuth testXFTPServer2
|
||||
authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP AUTH)
|
||||
authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP (B.unpack $ strEncode testXFTPServer2) AUTH)
|
||||
it "should pass with correct password" $ testXFTPServerTest auth (srv auth) `shouldReturn` Nothing
|
||||
it "should fail without password" $ testXFTPServerTest auth (srv Nothing) `shouldReturn` authErr
|
||||
it "should fail with incorrect password" $ testXFTPServerTest auth (srv $ Just "wrong") `shouldReturn` authErr
|
||||
@@ -179,7 +179,7 @@ testXFTPAgentSendReceiveRedirect = withXFTPServer $ do
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
FileDescriptionURI {description} <- either fail pure $ strDecode uri
|
||||
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing True
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 65536 totalSize) -- extra RFPROG before switching to real file
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 8388608 totalSize)
|
||||
@@ -223,7 +223,7 @@ testXFTPAgentSendReceiveNoRedirect = withXFTPServer $ do
|
||||
FileDescriptionURI {description} <- either fail pure $ strDecode uri
|
||||
let ValidFileDescription FileDescription {redirect} = description
|
||||
redirect `shouldBe` Nothing
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing True
|
||||
-- NO extra "RFPROG 65k 65k" before switching to real file
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 5242880 totalSize)
|
||||
@@ -311,7 +311,7 @@ testReceive' rcp rfd originalFilePath = testReceiveCF' rcp rfd Nothing originalF
|
||||
|
||||
testReceiveCF' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> Int64 -> ExceptT AgentErrorType IO RcvFileId
|
||||
testReceiveCF' rcp rfd cfArgs originalFilePath size = do
|
||||
rfId <- xftpReceiveFile rcp 1 rfd cfArgs
|
||||
rfId <- xftpReceiveFile rcp 1 rfd cfArgs True
|
||||
rfProgress rcp size
|
||||
("", rfId', RFDONE path) <- rfGet rcp
|
||||
liftIO $ do
|
||||
@@ -336,7 +336,7 @@ testXFTPAgentReceiveRestore = do
|
||||
-- receive file - should not succeed with server down
|
||||
rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do
|
||||
xftpStartWorkers rcp (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp 1 rfd Nothing
|
||||
rfId <- xftpReceiveFile rcp 1 rfd Nothing True
|
||||
liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt
|
||||
pure rfId
|
||||
|
||||
@@ -380,7 +380,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
-- receive file - should not succeed with server down
|
||||
rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do
|
||||
xftpStartWorkers rcp (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp 1 rfd Nothing
|
||||
rfId <- xftpReceiveFile rcp 1 rfd Nothing True
|
||||
liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt
|
||||
pure rfId
|
||||
|
||||
@@ -392,7 +392,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
-- receive file - should fail with AUTH error
|
||||
withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp'
|
||||
("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp'
|
||||
rfId' `shouldBe` rfId
|
||||
|
||||
-- tmp path should be removed after permanent error
|
||||
@@ -471,7 +471,8 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
-- send file - should fail with AUTH error
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
("", sfId', SFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- sfGet sndr'
|
||||
("", sfId', SFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <-
|
||||
sfGet sndr'
|
||||
sfId' `shouldBe` sfId
|
||||
|
||||
-- prefix path should be removed after permanent error
|
||||
@@ -505,8 +506,9 @@ testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $
|
||||
-- receive file - should fail with AUTH error
|
||||
withAgent 3 agentCfg initAgentServers testDB2 $ \rcp2 -> runRight $ do
|
||||
xftpStartWorkers rcp2 (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2
|
||||
rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True
|
||||
("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <-
|
||||
rfGet rcp2
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
|
||||
testXFTPAgentDeleteRestore :: HasCallStack => IO ()
|
||||
@@ -542,8 +544,9 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
-- receive file - should fail with AUTH error
|
||||
withAgent 5 agentCfg initAgentServers testDB3 $ \rcp2 -> runRight $ do
|
||||
xftpStartWorkers rcp2 (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2
|
||||
rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True
|
||||
("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <-
|
||||
rfGet rcp2
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
|
||||
testXFTPAgentDeleteOnServer :: HasCallStack => IO ()
|
||||
@@ -576,8 +579,9 @@ testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $
|
||||
|
||||
runRight_ . void $ do
|
||||
-- receive file 1 again
|
||||
rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing
|
||||
("", rfId1', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp
|
||||
rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing True
|
||||
("", rfId1', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <-
|
||||
rfGet rcp
|
||||
liftIO $ rfId1 `shouldBe` rfId1'
|
||||
|
||||
-- receive file 2
|
||||
@@ -608,8 +612,9 @@ testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do
|
||||
|
||||
-- receive file 1 again - should fail with AUTH error
|
||||
runRight $ do
|
||||
rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp
|
||||
rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing True
|
||||
("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <-
|
||||
rfGet rcp
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
|
||||
-- create and send file 2
|
||||
|
||||
@@ -14,6 +14,7 @@ import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Server (runXFTPServerBlocking)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes)
|
||||
import Simplex.FileTransfer.Transport (supportedFileServerVRange)
|
||||
import Simplex.Messaging.Protocol (XFTPServer)
|
||||
import Simplex.Messaging.Transport (ALPN)
|
||||
import Simplex.Messaging.Transport.Server
|
||||
@@ -118,6 +119,7 @@ testXFTPServerConfig_ alpn =
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
|
||||
Reference in New Issue
Block a user