mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-08-30 09:48:23 +00:00
Compare commits
59
Commits
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
# 5.6.2
|
||||
|
||||
Version 5.6.2.2.
|
||||
|
||||
SMP agent:
|
||||
- Lower memory consumption (~20-25%).
|
||||
- More stable XFTP file uploads and downloads.
|
||||
- API to receive network connectivity changes from the apps.
|
||||
- to reduce battery consumption: connection attempts interval growing to every 2 hours when app reports as offline.
|
||||
- to reduce retries and traffic: 50% increased timeouts when on mobile network.
|
||||
|
||||
XFTP server:
|
||||
- expire files on start.
|
||||
- version negotiation based on TLS ALPN and handshake.
|
||||
|
||||
NTF server:
|
||||
- reduced downtime by ~100x faster start time.
|
||||
- exclude test tokens from statistics.
|
||||
|
||||
# 5.6.1
|
||||
|
||||
Version 5.6.1.0.
|
||||
|
||||
- Much faster iOS notification server start time (fewer skipped notifications).
|
||||
- Fix SMP server stored message stats.
|
||||
- Prevent overwriting uploaded XFTP files with subsequent upload attempts.
|
||||
- Faster base64 encoding/parsing.
|
||||
- Control port audit log and authentication.
|
||||
|
||||
# 5.6.0
|
||||
|
||||
Version 5.6.0.4.
|
||||
|
||||
SMP protocol/client/server:
|
||||
- support deniable sender command authorization (to be enabled in the next version).
|
||||
- remove support for SMP protocol versions (prior to v4, 07/2022).
|
||||
|
||||
Agent:
|
||||
- optional post-quantum key agreement using sntrup761 in double ratchet protocol.
|
||||
- improve performance of deleting multiple connections and files by batching database operations.
|
||||
- delay connection deletion to deliver pending messages.
|
||||
- API to test for notifications server.
|
||||
- remove support for client protocols versions (prior to 10/2022).
|
||||
|
||||
XFTP server:
|
||||
- restore storage quota in case of failed uploads.
|
||||
|
||||
Performance and stability improvements.
|
||||
|
||||
# 5.5.3
|
||||
|
||||
Agent:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
@@ -38,7 +39,8 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
-- Warning: this SMP agent server is experimental - it does not work correctly with multiple connected TCP clients in some cases.
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
|
||||
let AgentConfig {tcpPort} = cfg
|
||||
putStrLn $ maybe (error "no agent port") (\port -> "SMP agent listening on port " ++ port) tcpPort
|
||||
setLogLevel LogInfo -- LogError
|
||||
Right st <- createAgentStore agentDbFile agentDbKey False MCConsole
|
||||
withGlobalLogging logCfg $ runSMPAgent (transport @TLS) cfg servers st
|
||||
|
||||
@@ -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
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 5.6.0.1
|
||||
version: 5.6.2.2
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
@@ -169,6 +169,11 @@ tests:
|
||||
- silently == 1.2.*
|
||||
- main-tester == 0.2.*
|
||||
- timeit == 2.0.*
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-A64M
|
||||
- -with-rtsopts=-N1
|
||||
|
||||
ghc-options:
|
||||
# - -haddock
|
||||
@@ -179,3 +184,7 @@ ghc-options:
|
||||
- -Wincomplete-record-updates
|
||||
- -Wincomplete-uni-patterns
|
||||
- -Wunused-type-patterns
|
||||
- -O2
|
||||
|
||||
default-extensions:
|
||||
- StrictData
|
||||
|
||||
@@ -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,27 +167,32 @@ 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).
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Relay metadata and SimpleX network decentralization
|
||||
|
||||
## Problem
|
||||
|
||||
Currently, the clients configure/choose which servers to use, but they cannot see who operates them, in which geography and hosting provider, what is the server source code (in case it was modified from the reference implementation we provide) and also any administrative and feedback contacts.
|
||||
|
||||
Further, we currently use simplex.chat domain to host group links, and as diversity of the groups grows it is beginning to require managing feedback from the users about groups. It is important that this feedback is directed to relay owners and not to us, in case they are not our relays, as we are simply providing software here.
|
||||
|
||||
We also need to make it much easier to access administrative actions - e.g., if the relay owner wants to block some address based on the feedback, they should be able to do so effectively. While freedom of speech of users is important the autonomy and sovereignty of relay operators allowing them to decide what they want or don't want to host, irrespective of the reasons, is as important - public relays constitute a public space rather than a service, and while there is no authentication or authorization required to use them, the relay owners should be able to decide their conditions and limitations of use, as is the case for any public space.
|
||||
|
||||
## Why it is important
|
||||
|
||||
1. We are building network and not a service, so contrary to the traditional approach when service collects users metadata and conceals its own, it appears important to do the opposite - expose all relay metadata that may have any effect on users privacy and security and provide access to this metadata with the minimal friction, via the app UI.
|
||||
2. We need to remove the only remaining bit of centralization - using simplex.chat website to show the QR code and help new users onboarding - this role can be delegated to the relays.
|
||||
3. We need to make it easy for the users to submit feedback and to the relay operators to manage that feedback. That includes ourselves, as relay operators, and also any other person or organization that operates public relays.
|
||||
4. It is important to stress the requirement of AGPLv3 license to provide access to the source code to the relay users, as any code modifications might undermine user privacy and security and must be made transparent to the relay users.
|
||||
|
||||
While this document is not the end of the journey to decentralize the network, it's an important first step.
|
||||
|
||||
## Proposed solution
|
||||
|
||||
The proposed solution consists of two parts:
|
||||
|
||||
- communicate server metadata via protocol, so it can be observed by the clients.
|
||||
- create home page for the relays, with all the same metadata.
|
||||
- create invitation and address links in the same domain name as the relay.
|
||||
|
||||
The latter point is important so it is clear to the users who operates and owns the relay and where the access point to the content or group is hosted. Even though simplex.chat domain is never accessed by the app, and the meaningful part of the address is never sent to the page hosting server, it creates an impression of centralization, and some dependency on simplex.chat domain for anything other that showing the link QR code.
|
||||
|
||||
Moving invitation links to the domain of the relay (primary relay, in case the link has redundancy) will both clarify relay ownership, solve the incorrect mis-perception of centralization, remove the dependency on simplex-chat domain without any user effort, and provides the means to submit content complaints to the relay operators (should they wish to receive them, which seems reasonable for large public relays, but may be unnecessary for private relays where unidentified parties cannot create links).
|
||||
|
||||
## Solution details
|
||||
|
||||
Extend server INI file with information section:
|
||||
|
||||
```
|
||||
[INFORMATION]
|
||||
# Please note that under AGPLv3 license conditions you MUST make
|
||||
# any source code modifications available to the end users of the server.
|
||||
# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE
|
||||
# Not doing so would constitute a license violation.
|
||||
# Declaring an incorrect information here amounts to a fraud.
|
||||
# The license holders reserve the right to prosecute missing or incorrect
|
||||
# information about the server source code to the fullest extent permitted by the law.
|
||||
# The server will show warning on start if this field is absent
|
||||
# and will not launch from v6.0 until this field is added.
|
||||
# If any other information field is present, source code property also MUST be present.
|
||||
source_code: https://github.com/simplex-chat/simplexmq
|
||||
|
||||
# Declaring all below information is optional, any of these fields can be omitted.
|
||||
# Declaring an incorrect information here would amount to a fraud.
|
||||
|
||||
# We should split this document to the model one, where specific parameters will be external to the document,
|
||||
# and specific to us, so that relay operators can adopt our recommended policy and publish any amendments separately.
|
||||
conditions: https://github.com/simplex-chat/simplex-chat/blob/_archived-ep/ios-file-provider/PRIVACY.md
|
||||
# conditions_amendments: link
|
||||
|
||||
server_country: SE
|
||||
operator: SimpleX Chat Ltd.
|
||||
operator_country: GB # operator country
|
||||
website: https://simplex.chat
|
||||
admin_simplex: administrative SimpleX address
|
||||
admin_email: chat@simplex.chat
|
||||
admin_pgp: PGP key
|
||||
feedback_simplex: SimpleX address for feedback, comments and complaints
|
||||
feedback_email: complaints@simplex.chat
|
||||
feedback_pgp: PGP key
|
||||
hosting: Linode / Akamai Inc.
|
||||
hosting_country: US
|
||||
```
|
||||
|
||||
Server home page would show whether queue creation is allowed and/or password protected, server retention policy (e.g., preserve messages on restart or not, and persist connections or not).
|
||||
|
||||
Server queue address/contact pages will optionally, provide the UI to submit feedback, comments and complaints directly from the web page (not an MVP, initially we would simply show addresses for feedback, and, probably, create link that opens in the app with pre-populated message, and we could also use this addresses defined in server meta-data to submit feedback from inside of the app - it's also out of MVP scope).
|
||||
|
||||
If server is available on .onion address, the web pages would show "open via .onion" in Tor browser.
|
||||
|
||||
Extend server handshake header with these information fields:
|
||||
|
||||
```haskell
|
||||
data ServerHandshake = ServerHandshake
|
||||
{ smpVersionRange :: VersionRangeSMP,
|
||||
sessionId :: SessionId,
|
||||
-- pub key to agree shared secrets for command authorization and entity ID encryption.
|
||||
authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey),
|
||||
-- ^^ existing fields
|
||||
-- vv new field that will be sent as length-prefixed JSON in the handshake
|
||||
information :: Maybe ServerInformation
|
||||
}
|
||||
|
||||
data ServerInformation = ServerInformation
|
||||
{ -- below is based on the existing server configuration
|
||||
persistence :: SMPServerPersistenceMode,
|
||||
messageExpiration :: Int,
|
||||
statsEnabled :: Bool,
|
||||
newQueuesAllowed :: Bool,
|
||||
basicAuthEnabled :: Bool, -- server is private if enabled
|
||||
-- below is based on INFORMATION section of INI file
|
||||
sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license
|
||||
-- all below properties are optional, except entity name MUST be present if any entity country is present
|
||||
conditions :: Maybe ServerConditions,
|
||||
operator :: Maybe Entity,
|
||||
website :: Maybe Text,
|
||||
admin :: Maybe ServerContactAddress,
|
||||
feedback :: Maybe ServerContactAddress,
|
||||
hosting :: Maybe Entity,
|
||||
serverCountry :: Maybe Text
|
||||
}
|
||||
|
||||
data ServerPersistenceMode = SMPMemoryOnly | SPMQueues | SPMQueuesMessages
|
||||
|
||||
data ServerConditions = ServerConditions {conditions :: Text, amendments :: Maybe Text}
|
||||
|
||||
data Entity = Entity {name :: Text, country :: Maybe Text}
|
||||
|
||||
data ServerContactAddress = ServerContactAddress
|
||||
{ simplex :: Maybe Text,
|
||||
email :: Maybe Text, -- it is recommended that it matches DNS email address, if either is present
|
||||
pgp :: Maybe Text
|
||||
}
|
||||
```
|
||||
|
||||
This extended server information will be stored in the chat database every time it changes and shown in the UI of the server configuration.
|
||||
@@ -0,0 +1,120 @@
|
||||
# XFTP version agreement
|
||||
|
||||
## Problem
|
||||
|
||||
XFTP is using HTTP2 protocol for encoding requests and responses.
|
||||
Unlike SMP which has a connection handshake initiated by a server and signals available versions XFTP/HTTP2 is almost entirely client-driven.
|
||||
So, a client can only try to guess which protocol versions are supported by a server by sending a probe/hello request first.
|
||||
Determining the endpoint for such a request is an implicit version agreement by itself.
|
||||
Sending such a request to an old server would error out and requiring it from old clients would break them.
|
||||
|
||||
## Solution
|
||||
|
||||
The TLS layer used by the XFTP server has an optional [ALPN](https://datatracker.ietf.org/doc/html/rfc7301) extension which allows the client and server to negotiate protocols and store the decision in TLS session context.
|
||||
Unless a client and a server run ALPN-aware versions, they would default to the old "unversioned" protocol.
|
||||
|
||||
TLS extension content is a 65kb chunk, but ALPN standard breaks it into 254b-sized chunks making it unusable for things like key exchange.
|
||||
The exchange is still client-driven: the client proposes a list, and then a server callback picks one.
|
||||
In effect, this makes it usable only to signal that some application-level handshake is desired and supported.
|
||||
|
||||
## Implementation
|
||||
|
||||
ALPN can be used to negotiate for any TLS-based protocol, but the description will focus on XFTP.
|
||||
|
||||
TransportClientConfig gets a new `alpn :: Maybe [ALPN]` field so a TLS transport can use it during TLS client creation.
|
||||
XFTP client sets it to `Just ["xftp/1"]`.
|
||||
The exact value is not important as long it is in agreement with the server side, but ALPN RFC insists on it being an IANA-registered identifier.
|
||||
|
||||
XFTP server sets `onALPNClientSuggest` TLS hook to pick the protocol when it is provided.
|
||||
The `tls` library treats SHOULD from the RFC as MUST and does a client-side check that the server responded with one of the client-proposed protocols.
|
||||
|
||||
Upon connection, transport implementation invokes `getNegotiatedProtocol` and stores it in `tlsALPN :: Maybe ALPN` field of transport context.
|
||||
HTTP2 transport implementation using `withHTTP2` passes negotiated "protocol" to client and server setup callbacks where they store it in their respective wrappers along with TLS session ID.
|
||||
A server request handler then knows by looking at the `sessionALPN` if it should require a "handshake" request first.
|
||||
A client code that got HTTP2Client with `sessionALPN` set knows if it has to proceed with handshake request.
|
||||
A handshake request still has to be initiated by a client, so it should be kept minimal, just enough data to pass the initiative to a server.
|
||||
A reply to that initial request should contain a server version range for the client to pick.
|
||||
A client then commits to a version, sending its part of a handshake.
|
||||
|
||||
In the future ALPN negotiation can be dropped in favor of mandatory handshakes or used to signal further handshake schemes.
|
||||
|
||||
The XFTP handshake data types and validation code are cloned from SMP.
|
||||
Currently they carry version information and session authentication parameters.
|
||||
Authentication parameters made mandatory as this exchange is guarded by the handshake version.
|
||||
|
||||
### Server side
|
||||
|
||||
`runHTTP2Server` callback used by `xftpServer` should get access to the session state to track handshakes.
|
||||
A local `TMap SessionId Handshake` is enough to switch request handlers.
|
||||
The HTTP2 server framework is extended with a way to signal client disconnection to remove sessions from this map.
|
||||
|
||||
The `Handshake` type mimics implicit state in stream based handshakes of SMP and NTF.
|
||||
|
||||
```haskell
|
||||
data Handshake
|
||||
= HandshakeSent C.PrivateKeyX25519 -- server private key that will be merged with client public in `THandleAuth`
|
||||
| HandshakeAccepted THandleAuth VersionXFTP -- session steady state after handshakes
|
||||
```
|
||||
|
||||
An HTTP2 request without ALPN is treated as legacy and requires no session entry.
|
||||
Its `Request`s are marked with `THandleParams {thVersion = VersionXFTP 1, ..}`.
|
||||
|
||||
An HTTP2 request with ALPN requires a session lookup.
|
||||
- A lack of entry indicates that a client must send an empty request, to which the server replies with its "server handshake" block and stores its private state in `HandshakeSent`.
|
||||
- If the session entry contains `HandshakeSent`, then the only valid request content is the "client handshake" block.
|
||||
The server validates client handshake (in the same way as SMP) and stores authentication and version in `HandshakeAccepted`
|
||||
- If the session entry contains `HandshakeAccepted`, then the server just passes it to `THandleParams`.
|
||||
|
||||
### Client side
|
||||
|
||||
`getXFTPClient` tweaks its transport config to include the ALPN marker and then checks if the client got its `sessionALPN` value.
|
||||
If there's a value set, it then sends an initial block and checks out the server handshake in response.
|
||||
After validation, it sends "client handshake" request to finish version negotiation.
|
||||
|
||||
```haskell
|
||||
let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = Just ["xftp/1"]}
|
||||
-- ...
|
||||
http2Client <- liftEitherError xftpClientError $ getVerifiedHTTP2Client -- ...
|
||||
thVersion <- case sessionALPN http2Client of
|
||||
Nothing -> pure $ VersionXFTP 1
|
||||
Just proto -> negotiate http2Client proto
|
||||
```
|
||||
|
||||
The resulting `XFTPClient` then contains a negotiated version and can be used to send transmissions with a more recent encoding.
|
||||
|
||||
## Block encoding
|
||||
|
||||
### Client Hello (request)
|
||||
|
||||
A request with an empty body and no padding.
|
||||
|
||||
### Server handshake (response)
|
||||
|
||||
SMP-encoded and padded to `xftpBlockSize` (~16kb).
|
||||
|
||||
```haskell
|
||||
data XFTPServerHandshake = XFTPServerHandshake
|
||||
{ xftpVersionRange :: VersionRangeXFTP,
|
||||
sessionId :: SessionId, -- validated by client against TLS unique
|
||||
authPubKey ::
|
||||
( X.CertificateChain, -- fingerprint validated by client against pre-shared hash
|
||||
X.SignedExact X.PubKey -- signature validated by client against server key from TLS
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Client handshake (request)
|
||||
|
||||
SMP-encoded and padded to `xftpBlockSize` (~16kb).
|
||||
|
||||
```haskell
|
||||
data XFTPClientHandshake = XFTPClientHandshake
|
||||
{ xftpVersion :: VersionXFTP,
|
||||
keyHash :: C.KeyHash, -- validated by server against its own cert fingerprint
|
||||
authPubKey :: C.PublicKeyX25519
|
||||
}
|
||||
```
|
||||
|
||||
### Server confirmation (response)
|
||||
|
||||
A response with an empty body and no padding.
|
||||
+24
-8
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 5.6.0.1
|
||||
version: 5.6.2.2
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -147,6 +147,7 @@ library
|
||||
Simplex.Messaging.Server.Stats
|
||||
Simplex.Messaging.Server.StoreLog
|
||||
Simplex.Messaging.ServiceScheme
|
||||
Simplex.Messaging.Session
|
||||
Simplex.Messaging.TMap
|
||||
Simplex.Messaging.Transport
|
||||
Simplex.Messaging.Transport.Buffer
|
||||
@@ -171,7 +172,9 @@ library
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
src
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2
|
||||
include-dirs:
|
||||
cbits
|
||||
c-sources:
|
||||
@@ -250,7 +253,9 @@ executable ntf-server
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/ntf-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -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
|
||||
@@ -323,7 +328,9 @@ executable smp-agent
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/smp-agent
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -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
|
||||
@@ -396,7 +403,9 @@ executable smp-server
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/smp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -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
|
||||
@@ -469,7 +478,9 @@ executable xftp
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/xftp
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -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
|
||||
@@ -542,7 +553,9 @@ executable xftp-server
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/xftp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -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
|
||||
@@ -639,6 +652,7 @@ test-suite simplexmq-test
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
SMPClient
|
||||
SMPProxyTests
|
||||
Util
|
||||
XFTPAgent
|
||||
XFTPCLI
|
||||
@@ -647,7 +661,9 @@ test-suite simplexmq-test
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
tests
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
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
|
||||
build-depends:
|
||||
HUnit ==1.6.*
|
||||
, QuickCheck ==2.14.*
|
||||
|
||||
+110
-100
@@ -35,6 +35,7 @@ import Control.Monad.Reader
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Coerce (coerce)
|
||||
import Data.Composition ((.:))
|
||||
import Data.Either (rights)
|
||||
import Data.Int (Int64)
|
||||
@@ -73,8 +74,9 @@ import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM)
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
startXFTPWorkers :: AgentMonad m => AgentClient -> Maybe FilePath -> m ()
|
||||
startXFTPWorkers :: AgentClient -> Maybe FilePath -> AM ()
|
||||
startXFTPWorkers c workDir = do
|
||||
wd <- asks $ xftpWorkDir . xftpAgent
|
||||
atomically $ writeTVar wd workDir
|
||||
@@ -83,23 +85,26 @@ startXFTPWorkers c workDir = do
|
||||
startSndFiles cfg
|
||||
startDelFiles cfg
|
||||
where
|
||||
startRcvFiles :: AgentConfig -> AM ()
|
||||
startRcvFiles AgentConfig {rcvFilesTTL} = do
|
||||
pendingRcvServers <- withStore' c (`getPendingRcvFilesServers` rcvFilesTTL)
|
||||
forM_ pendingRcvServers $ \s -> resumeXFTPRcvWork c (Just s)
|
||||
lift . forM_ pendingRcvServers $ \s -> resumeXFTPRcvWork c (Just s)
|
||||
-- start local worker for files pending decryption,
|
||||
-- no need to make an extra query for the check
|
||||
-- as the worker will check the store anyway
|
||||
resumeXFTPRcvWork c Nothing
|
||||
lift $ resumeXFTPRcvWork c Nothing
|
||||
startSndFiles :: AgentConfig -> AM ()
|
||||
startSndFiles AgentConfig {sndFilesTTL} = do
|
||||
-- start worker for files pending encryption/creation
|
||||
resumeXFTPSndWork c Nothing
|
||||
lift $ resumeXFTPSndWork c Nothing
|
||||
pendingSndServers <- withStore' c (`getPendingSndFilesServers` sndFilesTTL)
|
||||
forM_ pendingSndServers $ \s -> resumeXFTPSndWork c (Just s)
|
||||
lift . forM_ pendingSndServers $ \s -> resumeXFTPSndWork c (Just s)
|
||||
startDelFiles :: AgentConfig -> AM ()
|
||||
startDelFiles AgentConfig {rcvFilesTTL} = do
|
||||
pendingDelServers <- withStore' c (`getPendingDelFilesServers` rcvFilesTTL)
|
||||
forM_ pendingDelServers $ resumeXFTPDelWork c
|
||||
lift . forM_ pendingDelServers $ resumeXFTPDelWork c
|
||||
|
||||
closeXFTPAgent :: MonadUnliftIO m => XFTPAgent -> m ()
|
||||
closeXFTPAgent :: XFTPAgent -> IO ()
|
||||
closeXFTPAgent a = do
|
||||
stopWorkers $ xftpRcvWorkers a
|
||||
stopWorkers $ xftpSndWorkers a
|
||||
@@ -107,16 +112,16 @@ closeXFTPAgent a = do
|
||||
where
|
||||
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
|
||||
|
||||
xftpReceiveFile' :: AgentMonad m => AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> m RcvFileId
|
||||
xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AM RcvFileId
|
||||
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs = do
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "rcv.xftp"
|
||||
prefixPath <- lift $ getPrefixPath "rcv.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
relTmpPath = relPrefixPath </> "xftp.encrypted"
|
||||
relSavePath = relPrefixPath </> "xftp.decrypted"
|
||||
createDirectory =<< toFSFilePath relTmpPath
|
||||
createEmptyFile =<< toFSFilePath relSavePath
|
||||
lift $ createDirectory =<< toFSFilePath relTmpPath
|
||||
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
|
||||
@@ -124,8 +129,8 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi
|
||||
-- prepare description paths
|
||||
let relTmpPathRedirect = relPrefixPath </> "xftp.redirect-encrypted"
|
||||
relSavePathRedirect = relPrefixPath </> "xftp.redirect-decrypted"
|
||||
createDirectory =<< toFSFilePath relTmpPathRedirect
|
||||
createEmptyFile =<< toFSFilePath relSavePathRedirect
|
||||
lift $ createDirectory =<< toFSFilePath relTmpPathRedirect
|
||||
lift $ createEmptyFile =<< toFSFilePath relSavePathRedirect
|
||||
cfArgsRedirect <- atomically $ CF.randomArgs g
|
||||
let saveFileRedirect = CryptoFile relSavePathRedirect $ Just cfArgsRedirect
|
||||
-- create download tasks
|
||||
@@ -133,62 +138,63 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi
|
||||
forM_ chunks (downloadChunk c)
|
||||
pure fId
|
||||
|
||||
downloadChunk :: AgentMonad m => AgentClient -> FileChunk -> m ()
|
||||
downloadChunk :: AgentClient -> FileChunk -> AM ()
|
||||
downloadChunk c FileChunk {replicas = (FileChunkReplica {server} : _)} = do
|
||||
void $ getXFTPRcvWorker True c (Just server)
|
||||
lift . void $ getXFTPRcvWorker True c (Just server)
|
||||
downloadChunk _ _ = throwError $ INTERNAL "no replicas"
|
||||
|
||||
getPrefixPath :: AgentMonad m => String -> m FilePath
|
||||
getPrefixPath :: String -> AM' FilePath
|
||||
getPrefixPath suffix = do
|
||||
workPath <- getXFTPWorkPath
|
||||
ts <- liftIO getCurrentTime
|
||||
let isoTime = formatTime defaultTimeLocale "%Y%m%d_%H%M%S_%6q" ts
|
||||
uniqueCombine workPath (isoTime <> "_" <> suffix)
|
||||
|
||||
toFSFilePath :: AgentMonad m => FilePath -> m FilePath
|
||||
toFSFilePath :: FilePath -> AM' FilePath
|
||||
toFSFilePath f = (</> f) <$> getXFTPWorkPath
|
||||
|
||||
createEmptyFile :: AgentMonad m => FilePath -> m ()
|
||||
createEmptyFile :: FilePath -> AM' ()
|
||||
createEmptyFile fPath = liftIO $ B.writeFile fPath ""
|
||||
|
||||
resumeXFTPRcvWork :: AgentMonad' m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
resumeXFTPRcvWork :: AgentClient -> Maybe XFTPServer -> AM' ()
|
||||
resumeXFTPRcvWork = void .: getXFTPRcvWorker False
|
||||
|
||||
getXFTPRcvWorker :: AgentMonad' m => Bool -> AgentClient -> Maybe XFTPServer -> m Worker
|
||||
getXFTPRcvWorker :: Bool -> AgentClient -> Maybe XFTPServer -> AM' Worker
|
||||
getXFTPRcvWorker hasWork c server = do
|
||||
ws <- asks $ xftpRcvWorkers . xftpAgent
|
||||
getAgentWorker "xftp_rcv" hasWork c server ws $
|
||||
maybe (runXFTPRcvLocalWorker c) (runXFTPRcvWorker c) server
|
||||
|
||||
runXFTPRcvWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> Worker -> m ()
|
||||
runXFTPRcvWorker :: AgentClient -> XFTPServer -> Worker -> AM ()
|
||||
runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> m ()
|
||||
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
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop ->
|
||||
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
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
when notifyOnRetry $ notify c rcvFileEntityId $ RFERR e
|
||||
closeXFTPServerClient c userId server digest
|
||||
liftIO $ closeXFTPServerClient c userId server digest
|
||||
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (show e)
|
||||
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> m ()
|
||||
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> AM ()
|
||||
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do
|
||||
fsFileTmpPath <- toFSFilePath fileTmpPath
|
||||
fsFileTmpPath <- lift $ toFSFilePath fileTmpPath
|
||||
chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
relChunkPath = fileTmpPath </> takeFileName chunkPath
|
||||
@@ -205,7 +211,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived
|
||||
pure (entityId, complete, RFPROG rcvd total)
|
||||
notify c entityId progress
|
||||
when complete . void $
|
||||
when complete . lift . void $
|
||||
getXFTPRcvWorker True c Nothing
|
||||
where
|
||||
receivedSize :: [RcvFileChunk] -> Int64
|
||||
@@ -221,37 +227,37 @@ withRetryIntervalLimit maxN ri action =
|
||||
withRetryIntervalCount ri $ \n delay loop ->
|
||||
when (n < maxN) $ action delay loop
|
||||
|
||||
retryOnError :: AgentMonad m => Text -> m a -> m a -> AgentErrorType -> m a
|
||||
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
|
||||
|
||||
rcvWorkerInternalError :: AgentMonad m => AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> m ()
|
||||
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> AM ()
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath internalErrStr = do
|
||||
forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr
|
||||
notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr
|
||||
|
||||
runXFTPRcvLocalWorker :: forall m. AgentMonad m => AgentClient -> Worker -> m ()
|
||||
runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM ()
|
||||
runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> m ()
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
runXFTPOperation AgentConfig {rcvFilesTTL} =
|
||||
withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $
|
||||
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
|
||||
decryptFile f `catchAgentError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show)
|
||||
decryptFile :: RcvFile -> m ()
|
||||
decryptFile :: RcvFile -> AM ()
|
||||
decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do
|
||||
let CryptoFile savePath cfArgs = saveFile
|
||||
fsSavePath <- toFSFilePath savePath
|
||||
when (status == RFSDecrypting) $
|
||||
fsSavePath <- lift $ toFSFilePath savePath
|
||||
lift . when (status == RFSDecrypting) $
|
||||
whenM (doesFileExist fsSavePath) (removeFile fsSavePath >> createEmptyFile fsSavePath)
|
||||
withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting
|
||||
chunkPaths <- getChunkPaths chunks
|
||||
@@ -264,16 +270,16 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
case redirect of
|
||||
Nothing -> do
|
||||
notify c rcvFileEntityId $ RFDONE fsSavePath
|
||||
forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
atomically $ waitUntilForeground c
|
||||
withStore' c (`updateRcvFileComplete` rcvFileId)
|
||||
Just RcvFileRedirect {redirectFileInfo, redirectDbId} -> do
|
||||
let RedirectFileInfo {size = redirectSize, digest = redirectDigest} = redirectFileInfo
|
||||
forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
atomically $ waitUntilForeground c
|
||||
withStore' c (`updateRcvFileComplete` rcvFileId)
|
||||
-- proceed with redirect
|
||||
yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `finally` (toFSFilePath fsSavePath >>= removePath)
|
||||
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"
|
||||
Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest})
|
||||
@@ -284,19 +290,19 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
withStore c $ \db -> updateRcvFileRedirect db redirectDbId next
|
||||
forM_ nextChunks (downloadChunk c)
|
||||
where
|
||||
getChunkPaths :: [RcvFileChunk] -> m [FilePath]
|
||||
getChunkPaths :: [RcvFileChunk] -> AM [FilePath]
|
||||
getChunkPaths [] = pure []
|
||||
getChunkPaths (RcvFileChunk {chunkTmpPath = Just path} : cs) = do
|
||||
ps <- getChunkPaths cs
|
||||
fsPath <- toFSFilePath path
|
||||
fsPath <- lift $ toFSFilePath path
|
||||
pure $ fsPath : ps
|
||||
getChunkPaths (RcvFileChunk {chunkTmpPath = Nothing} : _cs) =
|
||||
throwError $ INTERNAL "no chunk path"
|
||||
|
||||
xftpDeleteRcvFile' :: AgentMonad m => AgentClient -> RcvFileId -> m ()
|
||||
xftpDeleteRcvFile' :: AgentClient -> RcvFileId -> AM' ()
|
||||
xftpDeleteRcvFile' c rcvFileEntityId = xftpDeleteRcvFiles' c [rcvFileEntityId]
|
||||
|
||||
xftpDeleteRcvFiles' :: forall m. AgentMonad m => AgentClient -> [RcvFileId] -> m ()
|
||||
xftpDeleteRcvFiles' :: AgentClient -> [RcvFileId] -> AM' ()
|
||||
xftpDeleteRcvFiles' c rcvFileEntityIds = do
|
||||
rcvFiles <- rights <$> withStoreBatch c (\db -> map (fmap (first storeError) . getRcvFileByEntityId db) rcvFileEntityIds)
|
||||
redirects <- rights <$> batchFiles getRcvFileRedirects rcvFiles
|
||||
@@ -308,29 +314,29 @@ xftpDeleteRcvFiles' c rcvFileEntityIds = do
|
||||
(removePath . (workPath </>)) prefixPath `catchAll_` pure ()
|
||||
where
|
||||
fileComplete RcvFile {status} = status == RFSComplete || status == RFSError
|
||||
batchFiles :: (DB.Connection -> DBRcvFileId -> IO a) -> [RcvFile] -> m [Either AgentErrorType a]
|
||||
batchFiles :: (DB.Connection -> DBRcvFileId -> IO a) -> [RcvFile] -> AM' [Either AgentErrorType a]
|
||||
batchFiles f rcvFiles = withStoreBatch' c $ \db -> map (\RcvFile {rcvFileId} -> f db rcvFileId) rcvFiles
|
||||
|
||||
notify :: forall m e. (MonadUnliftIO m, AEntityI e) => AgentClient -> EntityId -> ACommand 'Agent e -> m ()
|
||||
notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> EntityId -> ACommand 'Agent e -> m ()
|
||||
notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, APC (sAEntity @e) cmd)
|
||||
|
||||
xftpSendFile' :: AgentMonad m => AgentClient -> UserId -> CryptoFile -> Int -> m SndFileId
|
||||
xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> AM SndFileId
|
||||
xftpSendFile' c userId file numRecipients = do
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "snd.xftp"
|
||||
prefixPath <- lift $ getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
key <- atomically $ C.randomSbKey g
|
||||
nonce <- atomically $ C.randomCbNonce g
|
||||
-- saving absolute filePath will not allow to restore file encryption after app update, but it's a short window
|
||||
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce Nothing
|
||||
void $ getXFTPSndWorker True c Nothing
|
||||
lift . void $ getXFTPSndWorker True c Nothing
|
||||
pure fId
|
||||
|
||||
xftpSendDescription' :: forall m. AgentMonad m => AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Int -> m SndFileId
|
||||
xftpSendDescription' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Int -> AM SndFileId
|
||||
xftpSendDescription' c userId (ValidFileDescription fdDirect@FileDescription {size, digest}) numRecipients = do
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "snd.xftp"
|
||||
prefixPath <- lift $ getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
let directYaml = prefixPath </> "direct.yaml"
|
||||
@@ -340,39 +346,39 @@ xftpSendDescription' c userId (ValidFileDescription fdDirect@FileDescription {si
|
||||
key <- atomically $ C.randomSbKey g
|
||||
nonce <- atomically $ C.randomCbNonce g
|
||||
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce $ Just RedirectFileInfo {size, digest}
|
||||
void $ getXFTPSndWorker True c Nothing
|
||||
lift . void $ getXFTPSndWorker True c Nothing
|
||||
pure fId
|
||||
|
||||
resumeXFTPSndWork :: AgentMonad' m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
resumeXFTPSndWork :: AgentClient -> Maybe XFTPServer -> AM' ()
|
||||
resumeXFTPSndWork = void .: getXFTPSndWorker False
|
||||
|
||||
getXFTPSndWorker :: AgentMonad' m => Bool -> AgentClient -> Maybe XFTPServer -> m Worker
|
||||
getXFTPSndWorker :: Bool -> AgentClient -> Maybe XFTPServer -> AM' Worker
|
||||
getXFTPSndWorker hasWork c server = do
|
||||
ws <- asks $ xftpSndWorkers . xftpAgent
|
||||
getAgentWorker "xftp_snd" hasWork c server ws $
|
||||
maybe (runXFTPSndPrepareWorker c) (runXFTPSndWorker c) server
|
||||
|
||||
runXFTPSndPrepareWorker :: forall m. AgentMonad m => AgentClient -> Worker -> m ()
|
||||
runXFTPSndPrepareWorker :: AgentClient -> Worker -> AM ()
|
||||
runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> m ()
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
runXFTPOperation cfg@AgentConfig {sndFilesTTL} =
|
||||
withWork c doWork (`getNextSndFileToPrepare` sndFilesTTL) $
|
||||
\f@SndFile {sndFileId, sndFileEntityId, prefixPath} ->
|
||||
prepareFile cfg f `catchAgentError` (sndWorkerInternalError c sndFileId sndFileEntityId prefixPath . show)
|
||||
prepareFile :: AgentConfig -> SndFile -> m ()
|
||||
prepareFile :: AgentConfig -> SndFile -> AM ()
|
||||
prepareFile _ SndFile {prefixPath = Nothing} =
|
||||
throwError $ INTERNAL "no prefix path"
|
||||
prepareFile cfg sndFile@SndFile {sndFileId, userId, prefixPath = Just ppath, status} = do
|
||||
SndFile {numRecipients, chunks} <-
|
||||
if status /= SFSEncrypted -- status is SFSNew or SFSEncrypting
|
||||
then do
|
||||
fsEncPath <- toFSFilePath $ sndFileEncPath ppath
|
||||
fsEncPath <- lift . toFSFilePath $ sndFileEncPath ppath
|
||||
when (status == SFSEncrypting) . whenM (doesFileExist fsEncPath) $
|
||||
removeFile fsEncPath
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSEncrypting
|
||||
@@ -384,39 +390,41 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
let numRecipients' = min numRecipients maxRecipients
|
||||
-- concurrently?
|
||||
-- separate worker to create chunks? record retries and delay on snd_file_chunks?
|
||||
forM_ (filter (not . chunkCreated) chunks) $ createChunk numRecipients'
|
||||
forM_ (filter (\SndFileChunk {replicas} -> null replicas) chunks) $ createChunk numRecipients'
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSUploading
|
||||
where
|
||||
AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients, messageRetryInterval = ri} = cfg
|
||||
encryptFileForUpload :: SndFile -> FilePath -> m (FileDigest, [(XFTPChunkSpec, FileDigest)])
|
||||
encryptFileForUpload SndFile {key, nonce, srcFile} fsEncPath = do
|
||||
encryptFileForUpload :: SndFile -> FilePath -> AM (FileDigest, [(XFTPChunkSpec, FileDigest)])
|
||||
encryptFileForUpload SndFile {key, nonce, srcFile, redirect} fsEncPath = do
|
||||
let CryptoFile {filePath} = srcFile
|
||||
fileName = takeFileName filePath
|
||||
fileSize <- liftIO $ fromInteger <$> CF.getFileContentsSize srcFile
|
||||
when (fileSize > maxFileSizeHard) $ throwError $ INTERNAL "max file size exceeded"
|
||||
let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing}
|
||||
fileSize' = fromIntegral (B.length fileHdr) + fileSize
|
||||
chunkSizes = prepareChunkSizes $ fileSize' + fileSizeLen + authTagSize
|
||||
chunkSizes' = map fromIntegral chunkSizes
|
||||
encSize = sum chunkSizes'
|
||||
payloadSize = fileSize' + fileSizeLen + authTagSize
|
||||
chunkSizes <- case redirect of
|
||||
Nothing -> pure $ prepareChunkSizes payloadSize
|
||||
Just _ -> case singleChunkSize payloadSize of
|
||||
Nothing -> throwError $ INTERNAL "max file size exceeded for redirect"
|
||||
Just chunkSize -> pure [chunkSize]
|
||||
let encSize = sum $ map fromIntegral chunkSizes
|
||||
void $ liftError (INTERNAL . show) $ encryptFile srcFile fileHdr key nonce fileSize' encSize fsEncPath
|
||||
digest <- liftIO $ LC.sha512Hash <$> LB.readFile fsEncPath
|
||||
let chunkSpecs = prepareChunkSpecs fsEncPath chunkSizes
|
||||
chunkDigests <- map FileDigest <$> mapM (liftIO . getChunkDigest) chunkSpecs
|
||||
pure (FileDigest digest, zip chunkSpecs chunkDigests)
|
||||
chunkCreated :: SndFileChunk -> Bool
|
||||
chunkCreated SndFileChunk {replicas} =
|
||||
any (\SndFileChunkReplica {replicaStatus} -> replicaStatus == SFRSCreated) replicas
|
||||
createChunk :: Int -> SndFileChunk -> m ()
|
||||
chunkDigests <- liftIO $ mapM getChunkDigest chunkSpecs
|
||||
pure (FileDigest digest, zip chunkSpecs $ coerce chunkDigests)
|
||||
createChunk :: Int -> SndFileChunk -> AM ()
|
||||
createChunk numRecipients' ch = do
|
||||
atomically $ assertAgentForeground c
|
||||
(replica, ProtoServerWithAuth srv _) <- tryCreate
|
||||
withStore' c $ \db -> createSndFileReplica db ch replica
|
||||
void $ getXFTPSndWorker True c (Just srv)
|
||||
lift . void $ getXFTPSndWorker True c (Just srv)
|
||||
where
|
||||
tryCreate = do
|
||||
usedSrvs <- newTVarIO ([] :: [XFTPServer])
|
||||
withRetryInterval (riFast ri) $ \_ loop ->
|
||||
withRetryInterval (riFast ri) $ \_ loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
createWithNextSrv usedSrvs
|
||||
`catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e
|
||||
where
|
||||
@@ -428,42 +436,43 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
replica <- agentXFTPNewChunk c ch numRecipients' srvAuth
|
||||
pure (replica, srvAuth)
|
||||
|
||||
sndWorkerInternalError :: AgentMonad m => AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> m ()
|
||||
sndWorkerInternalError :: AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> AM ()
|
||||
sndWorkerInternalError c sndFileId sndFileEntityId prefixPath internalErrStr = do
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
lift . forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c $ \db -> updateSndFileError db sndFileId internalErrStr
|
||||
notify c sndFileEntityId $ SFERR $ INTERNAL internalErrStr
|
||||
|
||||
runXFTPSndWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> Worker -> m ()
|
||||
runXFTPSndWorker :: AgentClient -> XFTPServer -> Worker -> AM ()
|
||||
runXFTPSndWorker c srv Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> m ()
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
runXFTPOperation cfg@AgentConfig {sndFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = do
|
||||
withWork c doWork (\db -> getNextSndChunkToUpload db srv sndFilesTTL) $ \case
|
||||
SndFileChunk {sndFileId, sndFileEntityId, filePrefixPath, replicas = []} -> sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) "chunk has no replicas"
|
||||
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 ->
|
||||
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
uploadFileChunk cfg fc replica
|
||||
`catchAgentError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
when notifyOnRetry $ notify c sndFileEntityId $ SFERR e
|
||||
closeXFTPServerClient c userId server digest
|
||||
liftIO $ closeXFTPServerClient c userId server digest
|
||||
withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) (show e)
|
||||
uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> m ()
|
||||
uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> AM ()
|
||||
uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do
|
||||
replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica
|
||||
fsFilePath <- toFSFilePath filePath
|
||||
fsFilePath <- lift $ toFSFilePath filePath
|
||||
unlessM (doesFileExist fsFilePath) $ throwError $ INTERNAL "encrypted file doesn't exist on upload"
|
||||
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
|
||||
atomically $ assertAgentForeground c
|
||||
@@ -479,10 +488,10 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
when complete $ do
|
||||
(sndDescr, rcvDescrs) <- sndFileToDescrs sf
|
||||
notify c sndFileEntityId $ SFDONE sndDescr rcvDescrs
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
lift . forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c $ \db -> updateSndFileComplete db sndFileId
|
||||
where
|
||||
addRecipients :: SndFileChunk -> SndFileChunkReplica -> m SndFileChunkReplica
|
||||
addRecipients :: SndFileChunk -> SndFileChunkReplica -> AM SndFileChunkReplica
|
||||
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {rcvIdsKeys}
|
||||
| length rcvIdsKeys > numRecipients = throwError $ INTERNAL "too many recipients"
|
||||
| length rcvIdsKeys == numRecipients = pure cr
|
||||
@@ -491,7 +500,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
rcvIdsKeys' <- agentXFTPAddRecipients c userId chunkDigest cr numRecipients'
|
||||
cr' <- withStore' c $ \db -> addSndChunkReplicaRecipients db cr $ L.toList rcvIdsKeys'
|
||||
addRecipients ch cr'
|
||||
sndFileToDescrs :: SndFile -> m (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
|
||||
sndFileToDescrs :: SndFile -> AM (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
|
||||
sndFileToDescrs SndFile {digest = Nothing} = throwError $ INTERNAL "snd file has no digest"
|
||||
sndFileToDescrs SndFile {chunks = []} = throwError $ INTERNAL "snd file has no chunks"
|
||||
sndFileToDescrs SndFile {digest = Just digest, key, nonce, chunks = chunks@(fstChunk : _), redirect} = do
|
||||
@@ -506,7 +515,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
fdRcvs = createRcvFileDescriptions fdRcv chunks
|
||||
validFdRcvs <- either (throwError . INTERNAL) pure $ mapM validateFileDescription fdRcvs
|
||||
pure (validFdSnd, validFdRcvs)
|
||||
toSndDescrChunk :: SndFileChunk -> m FileChunk
|
||||
toSndDescrChunk :: SndFileChunk -> AM FileChunk
|
||||
toSndDescrChunk SndFileChunk {replicas = []} = throwError $ INTERNAL "snd file chunk has no replicas"
|
||||
toSndDescrChunk ch@SndFileChunk {chunkNo, digest = chDigest, replicas = (SndFileChunkReplica {server, replicaId, replicaKey} : _)} = do
|
||||
let chunkSize = FileSize $ sndChunkSize ch
|
||||
@@ -557,10 +566,10 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
chunkUploaded SndFileChunk {replicas} =
|
||||
any (\SndFileChunkReplica {replicaStatus} -> replicaStatus == SFRSUploaded) replicas
|
||||
|
||||
deleteSndFileInternal :: AgentMonad m => AgentClient -> SndFileId -> m ()
|
||||
deleteSndFileInternal :: AgentClient -> SndFileId -> AM' ()
|
||||
deleteSndFileInternal c sndFileEntityId = deleteSndFilesInternal c [sndFileEntityId]
|
||||
|
||||
deleteSndFilesInternal :: forall m. AgentMonad m => AgentClient -> [SndFileId] -> m ()
|
||||
deleteSndFilesInternal :: AgentClient -> [SndFileId] -> AM' ()
|
||||
deleteSndFilesInternal c sndFileEntityIds = do
|
||||
sndFiles <- rights <$> withStoreBatch c (\db -> map (fmap (first storeError) . getSndFileByEntityId db) sndFileEntityIds)
|
||||
let (toDelete, toMarkDeleted) = partition fileComplete sndFiles
|
||||
@@ -571,15 +580,15 @@ deleteSndFilesInternal c sndFileEntityIds = do
|
||||
batchFiles_ updateSndFileDeleted toMarkDeleted
|
||||
where
|
||||
fileComplete SndFile {status} = status == SFSComplete || status == SFSError
|
||||
batchFiles_ :: (DB.Connection -> DBSndFileId -> IO a) -> [SndFile] -> m ()
|
||||
batchFiles_ :: (DB.Connection -> DBSndFileId -> IO a) -> [SndFile] -> AM' ()
|
||||
batchFiles_ f sndFiles = void $ withStoreBatch' c $ \db -> map (\SndFile {sndFileId} -> f db sndFileId) sndFiles
|
||||
|
||||
deleteSndFileRemote :: forall m. AgentMonad m => AgentClient -> UserId -> SndFileId -> ValidFileDescription 'FSender -> m ()
|
||||
deleteSndFileRemote :: AgentClient -> UserId -> SndFileId -> ValidFileDescription 'FSender -> AM' ()
|
||||
deleteSndFileRemote c userId sndFileEntityId sfd = deleteSndFilesRemote c userId [(sndFileEntityId, sfd)]
|
||||
|
||||
deleteSndFilesRemote :: forall m. AgentMonad m => AgentClient -> UserId -> [(SndFileId, ValidFileDescription 'FSender)] -> m ()
|
||||
deleteSndFilesRemote :: AgentClient -> UserId -> [(SndFileId, ValidFileDescription 'FSender)] -> AM' ()
|
||||
deleteSndFilesRemote c userId sndFileIdsDescrs = do
|
||||
deleteSndFilesInternal c (map fst sndFileIdsDescrs) `catchAgentError` (notify c "" . SFERR)
|
||||
deleteSndFilesInternal c (map fst sndFileIdsDescrs) `E.catchAny` (notify c "" . SFERR . INTERNAL . show)
|
||||
let rs = concatMap (mapMaybe chunkReplica . fdChunks . snd) sndFileIdsDescrs
|
||||
void $ withStoreBatch' c (\db -> map (uncurry $ createDeletedSndChunkReplica db userId) rs)
|
||||
let servers = S.fromList $ map (\(FileChunkReplica {server}, _) -> server) rs
|
||||
@@ -591,37 +600,38 @@ deleteSndFilesRemote c userId sndFileIdsDescrs = do
|
||||
FileChunk {digest, replicas = replica : _} -> Just (replica, digest)
|
||||
_ -> Nothing
|
||||
|
||||
resumeXFTPDelWork :: AgentMonad' m => AgentClient -> XFTPServer -> m ()
|
||||
resumeXFTPDelWork :: AgentClient -> XFTPServer -> AM' ()
|
||||
resumeXFTPDelWork = void .: getXFTPDelWorker False
|
||||
|
||||
getXFTPDelWorker :: AgentMonad' m => Bool -> AgentClient -> XFTPServer -> m Worker
|
||||
getXFTPDelWorker :: Bool -> AgentClient -> XFTPServer -> AM' Worker
|
||||
getXFTPDelWorker hasWork c server = do
|
||||
ws <- asks $ xftpDelWorkers . xftpAgent
|
||||
getAgentWorker "xftp_del" hasWork c server ws $ runXFTPDelWorker c server
|
||||
|
||||
runXFTPDelWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> Worker -> m ()
|
||||
runXFTPDelWorker :: AgentClient -> XFTPServer -> Worker -> AM ()
|
||||
runXFTPDelWorker c srv Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> m ()
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = do
|
||||
-- no point in deleting files older than rcv ttl, as they will be expired on server
|
||||
withWork c doWork (\db -> getNextDeletedSndChunkReplica db srv rcvFilesTTL) processDeletedReplica
|
||||
where
|
||||
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 ->
|
||||
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
deleteChunkReplica
|
||||
`catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
when notifyOnRetry $ notify c "" $ SFERR e
|
||||
closeXFTPServerClient c userId server chunkDigest
|
||||
liftIO $ closeXFTPServerClient c userId server chunkDigest
|
||||
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
@@ -630,7 +640,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
|
||||
agentXFTPDeleteChunk c userId replica
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
|
||||
delWorkerInternalError :: AgentMonad m => AgentClient -> Int64 -> AgentErrorType -> m ()
|
||||
delWorkerInternalError :: AgentClient -> Int64 -> AgentErrorType -> AM ()
|
||||
delWorkerInternalError c deletedSndChunkReplicaId e = do
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
notify c "" $ SFERR e
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Client where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
@@ -20,10 +23,12 @@ import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import qualified Network.HTTP.Types as N
|
||||
import qualified Network.HTTP2.Client as H
|
||||
import Simplex.FileTransfer.Description (mb)
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.FileTransfer.Server.Env (supportedXFTPhandshakes)
|
||||
import Simplex.FileTransfer.Transport
|
||||
import Simplex.Messaging.Client
|
||||
( NetworkConfig (..),
|
||||
@@ -37,6 +42,7 @@ import Simplex.Messaging.Client
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding (smpDecode, smpEncode)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
( BasicAuth,
|
||||
@@ -45,25 +51,27 @@ import Simplex.Messaging.Protocol
|
||||
RecipientId,
|
||||
SenderId,
|
||||
)
|
||||
import Simplex.Messaging.Transport (THandleParams (..), supportedParameters)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost)
|
||||
import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), 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, liftEitherError, whenM)
|
||||
import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM)
|
||||
import Simplex.Messaging.Version (compatibleVersion, pattern Compatible)
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
data XFTPClient = XFTPClient
|
||||
{ http2Client :: HTTP2Client,
|
||||
transportSession :: TransportSession FileResponse,
|
||||
thParams :: THandleParams XFTPVersion,
|
||||
thParams :: THandleParams XFTPVersion 'TClient,
|
||||
config :: XFTPClientConfig
|
||||
}
|
||||
|
||||
data XFTPClientConfig = XFTPClientConfig
|
||||
{ xftpNetworkConfig :: NetworkConfig,
|
||||
uploadTimeoutPerMb :: Int64
|
||||
serverVRange :: VersionRangeXFTP,
|
||||
clientALPN :: Maybe [ALPN]
|
||||
}
|
||||
|
||||
data XFTPChunkBody = XFTPChunkBody
|
||||
@@ -85,12 +93,13 @@ defaultXFTPClientConfig :: XFTPClientConfig
|
||||
defaultXFTPClientConfig =
|
||||
XFTPClientConfig
|
||||
{ xftpNetworkConfig = defaultNetworkConfig,
|
||||
uploadTimeoutPerMb = 10000000 -- 10 seconds
|
||||
serverVRange = supportedFileServerVRange,
|
||||
clientALPN = Just supportedXFTPhandshakes
|
||||
}
|
||||
|
||||
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkConfig} disconnected = runExceptT $ do
|
||||
let tcConfig = transportClientConfig xftpNetworkConfig
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
|
||||
let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = clientALPN}
|
||||
http2Config = xftpHTTP2Config tcConfig config
|
||||
username = proxyUsername transportSession
|
||||
ProtocolServer _ host port keyHash = srv
|
||||
@@ -98,13 +107,53 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkC
|
||||
clientVar <- newTVarIO Nothing
|
||||
let usePort = if null port then "443" else port
|
||||
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
|
||||
http2Client <- liftEitherError xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
|
||||
let HTTP2Client {sessionId} = http2Client
|
||||
thParams = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = currentXFTPVersion, thAuth = Nothing, implySessId = False, batch = True}
|
||||
c = XFTPClient {http2Client, thParams, transportSession, config}
|
||||
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}
|
||||
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)
|
||||
logDebug $ "Client negotiated protocol: " <> tshow thVersion
|
||||
let c = XFTPClient {http2Client, thParams, transportSession, config}
|
||||
atomically $ writeTVar clientVar $ Just c
|
||||
pure c
|
||||
|
||||
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
|
||||
sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash}
|
||||
pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v}
|
||||
where
|
||||
getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake
|
||||
getServerHandshake = do
|
||||
let helloReq = H.requestNoBody "POST" "/" []
|
||||
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 {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
|
||||
let (X.CertificateChain cert, exact) = serverAuth
|
||||
case cert of
|
||||
[_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure ()
|
||||
_ -> throwError "bad certificate"
|
||||
pubKey <- maybe (throwError "bad server key type") (`C.verifyX509` exact) serverKey
|
||||
C.x509ToPublic (pubKey, []) >>= C.pubKey
|
||||
sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO ()
|
||||
sendClientHandshake chs = do
|
||||
chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize
|
||||
let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs'
|
||||
HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c chsReq Nothing
|
||||
unless (B.null bodyHead) $ throwError $ PCEResponseError HANDSHAKE
|
||||
liftHS = liftEitherWith (const $ PCEResponseError HANDSHAKE)
|
||||
|
||||
closeXFTPClient :: XFTPClient -> IO ()
|
||||
closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client
|
||||
|
||||
@@ -136,16 +185,18 @@ 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)
|
||||
sendXFTPTransmission XFTPClient {config, thParams, http2Client} t chunkSpec_ = do
|
||||
let req = H.requestStreaming N.methodPost "/" [] streamBody
|
||||
reqTimeout = (\XFTPChunkSpec {chunkSize} -> chunkTimeout config chunkSize) <$> chunkSpec_
|
||||
HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- liftEitherError xftpClientError $ sendRequest http2Client req reqTimeout
|
||||
reqTimeout = xftpReqTimeout config $ (\XFTPChunkSpec {chunkSize} -> chunkSize) <$> chunkSpec_
|
||||
HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- withExceptT xftpClientError . ExceptT $ sendRequest http2Client req (Just reqTimeout)
|
||||
when (B.length bodyHead /= xftpBlockSize) $ throwError $ PCEResponseError BLOCK
|
||||
-- TODO validate that the file ID is the same as in the request?
|
||||
(_, _, (_, _fId, respOrErr)) <- liftEither . first PCEResponseError $ xftpDecodeTransmission thParams bodyHead
|
||||
@@ -196,17 +247,22 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {
|
||||
let dhSecret = C.dh' sDhKey rpDhKey
|
||||
cbState <- liftEither . first PCECryptoError $ LC.cbInit dhSecret cbNonce
|
||||
let t = chunkTimeout config chunkSize
|
||||
t `timeout` download cbState >>= maybe (throwError PCEResponseTimeout) pure
|
||||
ExceptT (sequence <$> (t `timeout` download cbState)) >>= maybe (throwError PCEResponseTimeout) pure
|
||||
where
|
||||
download cbState =
|
||||
withExceptT PCEResponseError $
|
||||
runExceptT . withExceptT PCEResponseError $
|
||||
receiveEncFile chunkPart cbState chunkSpec `catchError` \e ->
|
||||
whenM (doesFileExist filePath) (removeFile filePath) >> throwError e
|
||||
_ -> throwError $ PCEResponseError NO_FILE
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
xftpReqTimeout :: XFTPClientConfig -> Maybe Word32 -> Int
|
||||
xftpReqTimeout cfg@XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout}} chunkSize_ =
|
||||
maybe tcpTimeout (chunkTimeout cfg) chunkSize_
|
||||
|
||||
chunkTimeout :: XFTPClientConfig -> Word32 -> Int
|
||||
chunkTimeout config chunkSize = fromIntegral $ (fromIntegral chunkSize * uploadTimeoutPerMb config) `div` mb 1
|
||||
chunkTimeout XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout, tcpTimeoutPerKb}} sz =
|
||||
tcpTimeout + fromIntegral (min ((fromIntegral sz `div` 1024) * tcpTimeoutPerKb) (fromIntegral (maxBound :: Int)))
|
||||
|
||||
deleteXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> SenderId -> ExceptT XFTPClientError IO ()
|
||||
deleteXFTPChunk c spKey sId = sendXFTPCommand c spKey sId FDEL Nothing >>= okResponse
|
||||
|
||||
@@ -10,6 +10,7 @@ module Simplex.FileTransfer.Client.Agent where
|
||||
import Control.Logger.Simple (logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans (lift)
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text (Text)
|
||||
@@ -86,7 +87,7 @@ getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
|
||||
waitForXFTPClient :: XFTPClientVar -> ME XFTPClient
|
||||
waitForXFTPClient clientVar = do
|
||||
let XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} = xftpConfig config
|
||||
client_ <- tcpConnectTimeout `timeout` atomically (readTMVar clientVar)
|
||||
client_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar clientVar)
|
||||
liftEither $ case client_ of
|
||||
Just (Right c) -> Right c
|
||||
Just (Left e) -> Left e
|
||||
@@ -110,7 +111,7 @@ getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
|
||||
TM.delete srv xftpClients
|
||||
throwError e
|
||||
tryConnectAsync :: ME ()
|
||||
tryConnectAsync = void . async $ do
|
||||
tryConnectAsync = void . lift . async . runExceptT $ do
|
||||
withRetryInterval (reconnectInterval config) $ \_ loop -> void $ tryConnectClient loop
|
||||
|
||||
showServer :: XFTPServer -> Text
|
||||
|
||||
@@ -16,6 +16,7 @@ module Simplex.FileTransfer.Client.Main
|
||||
xftpClientCLI,
|
||||
cliSendFile,
|
||||
cliSendFileOpts,
|
||||
singleChunkSize,
|
||||
prepareChunkSizes,
|
||||
prepareChunkSpecs,
|
||||
maxFileSize,
|
||||
@@ -36,13 +37,14 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Char (toLower)
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (fromMaybe, listToMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Data.Word (Word32)
|
||||
import GHC.Records (HasField (getField))
|
||||
@@ -320,7 +322,9 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
|
||||
-- the reason we don't do pooled downloads here within one server is that http2 library doesn't handle cleint concurrency, even though
|
||||
-- upload doesn't allow other requests within the same client until complete (but download does allow).
|
||||
logInfo $ "uploading " <> tshow (length chunks) <> " chunks..."
|
||||
map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 chunks' (mapM $ uploadFileChunk a)
|
||||
(errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 chunks' . mapM $ runExceptT . uploadFileChunk a)
|
||||
mapM_ throwError errs
|
||||
pure $ map snd (sortOn fst rs)
|
||||
where
|
||||
uploadFileChunk :: XFTPClientAgent -> (Int, XFTPChunkSpec, XFTPServerWithAuth) -> ExceptT CLIError IO (Int, SentFileChunk)
|
||||
uploadFileChunk a (chunkNo, chunkSpec@XFTPChunkSpec {chunkSize}, ProtoServerWithAuth xftpServer auth) = do
|
||||
@@ -413,7 +417,8 @@ getChunkDigest :: XFTPChunkSpec -> IO ByteString
|
||||
getChunkDigest XFTPChunkSpec {filePath = chunkPath, chunkOffset, chunkSize} =
|
||||
withFile chunkPath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
LC.sha256Hash <$> LB.hGet h (fromIntegral chunkSize)
|
||||
chunk <- LB.hGet h (fromIntegral chunkSize)
|
||||
pure $! LC.sha256Hash chunk
|
||||
|
||||
cliReceiveFile :: ReceiveOptions -> ExceptT CLIError IO ()
|
||||
cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, verbose, yes} =
|
||||
@@ -431,7 +436,9 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath,
|
||||
FileChunkReplica {server} : _ -> server
|
||||
srvChunks = groupAllOn srv chunks
|
||||
g <- liftIO C.newRandom
|
||||
chunkPaths <- map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 srvChunks (mapM $ downloadFileChunk g a encPath size downloadedChunks)
|
||||
(errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 srvChunks $ mapM $ runExceptT . downloadFileChunk g a encPath size downloadedChunks)
|
||||
mapM_ throwError errs
|
||||
let chunkPaths = map snd $ sortOn fst rs
|
||||
encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths
|
||||
when (encDigest /= unFileDigest digest) $ throwError $ CLIError "File digest mismatch"
|
||||
encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths
|
||||
@@ -528,6 +535,12 @@ getFileDescription' path =
|
||||
getFileDescription path >>= \case
|
||||
AVFD fd -> either (throwError . CLIError) pure $ checkParty fd
|
||||
|
||||
singleChunkSize :: Int64 -> Maybe Word32
|
||||
singleChunkSize size' =
|
||||
listToMaybe $ dropWhile (< chunkSize) serverChunkSizes
|
||||
where
|
||||
chunkSize = fromIntegral size'
|
||||
|
||||
prepareChunkSizes :: Int64 -> [Word32]
|
||||
prepareChunkSizes size' = prepareSizes size'
|
||||
where
|
||||
|
||||
@@ -29,7 +29,7 @@ import UnliftIO.Directory (removeFile)
|
||||
encryptFile :: CryptoFile -> ByteString -> C.SbKey -> C.CbNonce -> Int64 -> Int64 -> FilePath -> ExceptT FTCryptoError IO ()
|
||||
encryptFile srcFile fileHdr key nonce fileSize' encSize encFile = do
|
||||
sb <- liftEitherWith FTCECryptoError $ LC.sbInit key nonce
|
||||
CF.withFile srcFile ReadMode $ \r -> withFile encFile WriteMode $ \w -> do
|
||||
CF.withFile srcFile ReadMode $ \r -> ExceptT . withFile encFile WriteMode $ \w -> runExceptT $ do
|
||||
let lenStr = smpEncode fileSize'
|
||||
(hdr, !sb') = LC.sbEncryptChunk sb $ lenStr <> fileHdr
|
||||
padLen = encSize - authTagSize - fileSize' - 8
|
||||
|
||||
@@ -227,7 +227,7 @@ validateFileDescription fd@FileDescription {size, chunks}
|
||||
| otherwise = Right $ ValidFD fd
|
||||
where
|
||||
chunkNos = map (\FileChunk {chunkNo} -> chunkNo) chunks
|
||||
chunksSize = fromIntegral . foldl' (\s FileChunk {chunkSize} -> s + unFileSize chunkSize) 0
|
||||
chunksSize = foldl' (\(s :: Int64) FileChunk {chunkSize} -> s + fromIntegral (unFileSize chunkSize)) 0
|
||||
|
||||
encodeFileDescription :: FileDescription p -> YAMLFileDescription
|
||||
encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSize, chunks, redirect} =
|
||||
|
||||
@@ -25,7 +25,7 @@ import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word32)
|
||||
import Simplex.FileTransfer.Transport (VersionXFTP, XFTPErrorType (..), XFTPVersion, pattern VersionXFTP, xftpClientHandshake)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPVersion, xftpClientHandshakeStub)
|
||||
import Simplex.Messaging.Client (authTransmission)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
@@ -39,8 +39,8 @@ import Simplex.Messaging.Protocol
|
||||
ProtocolErrorType (..),
|
||||
ProtocolMsgTag (..),
|
||||
ProtocolType (..),
|
||||
RcvPublicDhKey,
|
||||
RcvPublicAuthKey,
|
||||
RcvPublicDhKey,
|
||||
RecipientId,
|
||||
SenderId,
|
||||
SentRawTransmission,
|
||||
@@ -48,19 +48,17 @@ import Simplex.Messaging.Protocol
|
||||
SndPublicAuthKey,
|
||||
Transmission,
|
||||
TransmissionForAuth (..),
|
||||
encodeTransmissionForAuth,
|
||||
CorrId (..),
|
||||
encodeTransmission,
|
||||
encodeTransmissionForAuth,
|
||||
messageTagP,
|
||||
tDecodeParseValidate,
|
||||
tEncodeBatch1,
|
||||
tParse,
|
||||
)
|
||||
import Simplex.Messaging.Transport (THandleParams (..), TransportError (..))
|
||||
import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
|
||||
currentXFTPVersion :: VersionXFTP
|
||||
currentXFTPVersion = VersionXFTP 1
|
||||
|
||||
xftpBlockSize :: Int
|
||||
xftpBlockSize = 16384
|
||||
|
||||
@@ -144,7 +142,7 @@ instance FilePartyI p => ProtocolMsgTag (FileCommandTag p) where
|
||||
instance Protocol XFTPVersion XFTPErrorType FileResponse where
|
||||
type ProtoCommand FileResponse = FileCmd
|
||||
type ProtoType FileResponse = 'PXFTP
|
||||
protocolClientHandshake = xftpClientHandshake
|
||||
protocolClientHandshake = xftpClientHandshakeStub
|
||||
protocolPing = FileCmd SFRecipient PING
|
||||
protocolError = \case
|
||||
FRErr e -> Just e
|
||||
@@ -328,12 +326,12 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of
|
||||
Just Refl -> Just c
|
||||
_ -> Nothing
|
||||
|
||||
xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString
|
||||
xftpEncodeAuthTransmission thParams pKey (corrId, fId, msg) = do
|
||||
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 Nothing (Just pKey) corrId tForAuth
|
||||
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) (C.cbNonce $ bs corrId) tForAuth
|
||||
|
||||
xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> Transmission c -> Either TransportError ByteString
|
||||
xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString
|
||||
xftpEncodeTransmission thParams (corrId, fId, msg) = do
|
||||
let t = encodeTransmission thParams (corrId, fId, msg)
|
||||
xftpEncodeBatch1 (Nothing, t)
|
||||
@@ -342,7 +340,7 @@ xftpEncodeTransmission thParams (corrId, fId, msg) = do
|
||||
xftpEncodeBatch1 :: SentRawTransmission -> Either TransportError ByteString
|
||||
xftpEncodeBatch1 t = first (const TELargeMsg) $ C.pad (tEncodeBatch1 t) xftpBlockSize
|
||||
|
||||
xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> ByteString -> Either XFTPErrorType (SignedTransmission e c)
|
||||
xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> ByteString -> Either XFTPErrorType (SignedTransmission e c)
|
||||
xftpDecodeTransmission thParams t = do
|
||||
t' <- first (const BLOCK) $ C.unPad t
|
||||
case tParse thParams t' of
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.FileTransfer.Server where
|
||||
|
||||
@@ -18,20 +19,21 @@ import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Base64.URL as B64
|
||||
import Data.ByteString.Builder (byteString)
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.X509 as X
|
||||
import GHC.IO.Handle (hSetNewlineMode)
|
||||
import GHC.Stats (getRTSStats)
|
||||
import qualified Network.HTTP.Types as N
|
||||
@@ -46,17 +48,22 @@ import Simplex.FileTransfer.Server.StoreLog
|
||||
import Simplex.FileTransfer.Transport
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (CorrId, RcvPublicDhKey, RcvPublicAuthKey, RecipientId, TransmissionAuth)
|
||||
import Simplex.Messaging.Protocol (CorrId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth)
|
||||
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdAuthorization)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.Stats
|
||||
import Simplex.Messaging.Transport (THandleParams (..))
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.Buffer (trimCR)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize)
|
||||
import Simplex.Messaging.Transport.HTTP2.Server
|
||||
import Simplex.Messaging.Transport.Server (runTCPServer)
|
||||
import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version (isCompatible)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath ((</>))
|
||||
import System.IO (hPrint, hPutStrLn, universalNewlineMode)
|
||||
@@ -67,13 +74,12 @@ import qualified UnliftIO.Exception as E
|
||||
|
||||
type M a = ReaderT XFTPEnv IO a
|
||||
|
||||
data XFTPTransportRequest =
|
||||
XFTPTransportRequest
|
||||
{ thParams :: THandleParams XFTPVersion,
|
||||
reqBody :: HTTP2Body,
|
||||
request :: H.Request,
|
||||
sendResponse :: H.Response -> IO ()
|
||||
}
|
||||
data XFTPTransportRequest = XFTPTransportRequest
|
||||
{ thParams :: THandleParamsXFTP 'TServer,
|
||||
reqBody :: HTTP2Body,
|
||||
request :: H.Request,
|
||||
sendResponse :: H.Response -> IO ()
|
||||
}
|
||||
|
||||
runXFTPServer :: XFTPServerConfig -> IO ()
|
||||
runXFTPServer cfg = do
|
||||
@@ -83,20 +89,75 @@ runXFTPServer cfg = do
|
||||
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> IO ()
|
||||
runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started)
|
||||
|
||||
data Handshake
|
||||
= HandshakeSent C.PrivateKeyX25519
|
||||
| HandshakeAccepted (THandleAuth 'TServer) VersionXFTP
|
||||
|
||||
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration} started = do
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do
|
||||
mapM_ (expireServerFiles Nothing) fileExpiration
|
||||
restoreServerStats
|
||||
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer
|
||||
where
|
||||
runServer :: M ()
|
||||
runServer = do
|
||||
serverParams <- asks tlsServerParams
|
||||
let (chain, pk) = tlsServerCredentials serverParams
|
||||
signKey <- liftIO $ case C.x509ToPrivate (pk, []) >>= C.privKey of
|
||||
Right pk' -> pure pk'
|
||||
Left e -> putStrLn ("servers has no valid key: " <> show e) >> exitFailure
|
||||
env <- ask
|
||||
liftIO $
|
||||
runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration $ \sessionId r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r xftpBlockSize
|
||||
let thParams = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = currentXFTPVersion, thAuth = Nothing, implySessId = False, batch = True}
|
||||
processRequest XFTPTransportRequest {thParams, request = r, reqBody, sendResponse} `runReaderT` env
|
||||
sessions <- atomically TM.empty
|
||||
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}
|
||||
req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse}
|
||||
flip runReaderT env $ case sessionALPN of
|
||||
Nothing -> processRequest req0
|
||||
Just "xftp/1" ->
|
||||
xftpServerHandshakeV1 chain signKey sessions req0 >>= \case
|
||||
Nothing -> pure () -- handshake response sent
|
||||
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
|
||||
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}
|
||||
either sendError pure r
|
||||
where
|
||||
processHello = do
|
||||
unless (B.null bodyHead) $ throwError HANDSHAKE
|
||||
(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}
|
||||
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
|
||||
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
|
||||
sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer))
|
||||
sendError err = do
|
||||
runExceptT (encodeXftp err) >>= \case
|
||||
Right bs -> liftIO . sendResponse $ H.responseBuilder N.ok200 [] bs
|
||||
Left _ -> logError $ "Error encoding handshake error: " <> tshow err
|
||||
pure Nothing
|
||||
encodeXftp :: Encoding a => a -> ExceptT XFTPErrorType (ReaderT XFTPEnv IO) Builder
|
||||
encodeXftp a = byteString <$> liftHS (C.pad (smpEncode a) xftpBlockSize)
|
||||
liftHS = liftEitherWith (const HANDSHAKE)
|
||||
|
||||
stopServer :: M ()
|
||||
stopServer = do
|
||||
@@ -109,28 +170,10 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
|
||||
expireFiles :: ExpirationConfig -> M ()
|
||||
expireFiles expCfg = do
|
||||
st <- asks store
|
||||
let interval = checkInterval expCfg * 1000000
|
||||
forever $ do
|
||||
liftIO $ threadDelay' interval
|
||||
old <- liftIO $ expireBeforeEpoch expCfg
|
||||
sIds <- M.keysSet <$> readTVarIO (files st)
|
||||
forM_ sIds $ \sId -> do
|
||||
threadDelay 100000
|
||||
atomically (expiredFilePath st sId old)
|
||||
>>= mapM_ (maybeRemove $ delete st sId)
|
||||
where
|
||||
maybeRemove del = maybe del (remove del)
|
||||
remove del filePath =
|
||||
ifM
|
||||
(doesFileExist filePath)
|
||||
((removeFile filePath >> del) `catch` \(e :: SomeException) -> logError $ "failed to remove expired file " <> tshow filePath <> ": " <> tshow e)
|
||||
del
|
||||
delete st sId = do
|
||||
withFileLog (`logDeleteFile` sId)
|
||||
void $ atomically $ deleteFile st sId
|
||||
FileServerStats {filesExpired} <- asks serverStats
|
||||
atomically $ modifyTVar' filesExpired (+ 1)
|
||||
expireServerFiles (Just 100000) expCfg
|
||||
|
||||
serverStatsThread_ :: XFTPServerConfig -> [M ()]
|
||||
serverStatsThread_ XFTPServerConfig {logStatsInterval = Just interval, logStatsStartTime, serverStatsLogFile} =
|
||||
@@ -197,17 +240,32 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
hSetBuffering h LineBuffering
|
||||
hSetNewlineMode h universalNewlineMode
|
||||
hPutStrLn h "XFTP server control port\n'help' for supported commands"
|
||||
cpLoop h
|
||||
role <- newTVarIO CPRNone
|
||||
cpLoop h role
|
||||
where
|
||||
cpLoop h = do
|
||||
s <- B.hGetLine h
|
||||
case strDecode $ trimCR s of
|
||||
cpLoop h role = do
|
||||
s <- trimCR <$> B.hGetLine h
|
||||
case strDecode s of
|
||||
Right CPQuit -> hClose h
|
||||
Right cmd -> processCP h cmd >> cpLoop h
|
||||
Left err -> hPutStrLn h ("error: " <> err) >> cpLoop h
|
||||
processCP h = \case
|
||||
Right cmd -> logCmd s cmd >> processCP h role cmd >> cpLoop h role
|
||||
Left err -> hPutStrLn h ("error: " <> err) >> cpLoop h role
|
||||
logCmd s cmd = when shouldLog $ logWarn $ "ControlPort: " <> tshow s
|
||||
where
|
||||
shouldLog = case cmd of
|
||||
CPAuth _ -> False
|
||||
CPHelp -> False
|
||||
CPQuit -> False
|
||||
CPSkip -> False
|
||||
_ -> True
|
||||
processCP h role = \case
|
||||
CPAuth auth -> atomically $ writeTVar role $! newRole cfg
|
||||
where
|
||||
newRole XFTPServerConfig {controlPortUserAuth = user, controlPortAdminAuth = admin}
|
||||
| Just auth == admin = CPRAdmin
|
||||
| Just auth == user = CPRUser
|
||||
| otherwise = CPRNone
|
||||
CPStatsRTS -> E.tryAny getRTSStats >>= either (hPrint h) (hPrint h)
|
||||
CPDelete fileId -> unliftIO u $ do
|
||||
CPDelete fileId -> withUserRole $ unliftIO u $ do
|
||||
fs <- asks store
|
||||
r <- runExceptT $ do
|
||||
let asSender = ExceptT . atomically $ getFile fs SFSender fileId
|
||||
@@ -218,6 +276,14 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
CPHelp -> hPutStrLn h "commands: stats-rts, delete, help, quit"
|
||||
CPQuit -> pure ()
|
||||
CPSkip -> pure ()
|
||||
where
|
||||
withUserRole action =
|
||||
readTVarIO role >>= \case
|
||||
CPRAdmin -> action
|
||||
CPRUser -> action
|
||||
_ -> do
|
||||
logError "Unauthorized control port command"
|
||||
hPutStrLn h "AUTH"
|
||||
|
||||
data ServerFile = ServerFile
|
||||
{ filePath :: FilePath,
|
||||
@@ -230,10 +296,11 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea
|
||||
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", "", FRErr BLOCK) Nothing
|
||||
| otherwise = do
|
||||
case xftpDecodeTransmission thParams bodyHead of
|
||||
Right (sig_, signed, (corrId, fId, cmdOrErr)) -> do
|
||||
Right (sig_, signed, (corrId, fId, cmdOrErr)) ->
|
||||
case cmdOrErr of
|
||||
Right cmd -> do
|
||||
verifyXFTPTransmission sig_ signed fId cmd >>= \case
|
||||
let THandleParams {thAuth} = thParams
|
||||
verifyXFTPTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) sig_ signed fId cmd >>= \case
|
||||
VRVerified req -> uncurry send =<< processXFTPRequest body req
|
||||
VRFailed -> send (FRErr AUTH) Nothing
|
||||
Left e -> send (FRErr e) Nothing
|
||||
@@ -241,7 +308,6 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea
|
||||
send resp = sendXFTPResponse (corrId, fId, resp)
|
||||
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
|
||||
where
|
||||
sendXFTPResponse :: (CorrId, XFTPFileId, FileResponse) -> Maybe ServerFile -> M ()
|
||||
sendXFTPResponse (corrId, fId, resp) serverFile_ = do
|
||||
let t_ = xftpEncodeTransmission thParams (corrId, fId, resp)
|
||||
liftIO $ sendResponse $ H.responseStreaming N.ok200 [] $ streamBody t_
|
||||
@@ -260,8 +326,8 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea
|
||||
|
||||
data VerificationResult = VRVerified XFTPRequest | VRFailed
|
||||
|
||||
verifyXFTPTransmission :: Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult
|
||||
verifyXFTPTransmission tAuth authorized fId cmd =
|
||||
verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult
|
||||
verifyXFTPTransmission auth_ tAuth authorized fId cmd =
|
||||
case cmd of
|
||||
FileCmd SFSender (FNEW file rcps auth') -> pure $ XFTPReqNew file rcps auth' `verifyWith` sndKey file
|
||||
FileCmd SFRecipient PING -> pure $ VRVerified XFTPReqPing
|
||||
@@ -276,7 +342,7 @@ verifyXFTPTransmission tAuth authorized fId cmd =
|
||||
Right (fr, k) -> XFTPReqCmd fId fr cmd `verifyWith` k
|
||||
_ -> maybe False (dummyVerifyCmd Nothing authorized) tAuth `seq` VRFailed
|
||||
-- TODO verify with DH authorization
|
||||
req `verifyWith` k = if verifyCmdAuthorization Nothing tAuth authorized k then VRVerified req else VRFailed
|
||||
req `verifyWith` k = if verifyCmdAuthorization auth_ tAuth authorized k then VRVerified req else VRFailed
|
||||
|
||||
processXFTPRequest :: HTTP2Body -> XFTPRequest -> M (FileResponse, Maybe ServerFile)
|
||||
processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
@@ -307,7 +373,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
-- TODO validate body empty
|
||||
sId <- ExceptT $ addFileRetry st file 3 ts
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
withFileLog $ \sl -> do
|
||||
lift $ withFileLog $ \sl -> do
|
||||
logAddFile sl sId file ts
|
||||
logAddRecipients sl sId rcps
|
||||
stats <- asks serverStats
|
||||
@@ -339,57 +405,71 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
st <- asks store
|
||||
r <- runExceptT $ do
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
withFileLog $ \sl -> logAddRecipients sl sId rcps
|
||||
lift $ withFileLog $ \sl -> logAddRecipients sl sId rcps
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
|
||||
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
|
||||
pure $ FRRcvIds rIds
|
||||
pure $ either FRErr id r
|
||||
receiveServerFile :: FileRec -> M FileResponse
|
||||
receiveServerFile fr@FileRec {senderId, fileInfo} = case bodyPart of
|
||||
-- TODO do not allow repeated file upload
|
||||
receiveServerFile FileRec {senderId, fileInfo = FileInfo {size, digest}, filePath} = case bodyPart of
|
||||
Nothing -> pure $ FRErr SIZE
|
||||
Just getBody -> do
|
||||
-- TODO validate body size before downloading, once it's populated
|
||||
path <- asks $ filesPath . config
|
||||
let fPath = path </> B.unpack (B64.encode senderId)
|
||||
FileInfo {size, digest} = fileInfo
|
||||
withFileLog $ \sl -> logPutFile sl senderId fPath
|
||||
st <- asks store
|
||||
quota_ <- asks $ fileSizeQuota . config
|
||||
-- TODO timeout file upload, remove partially uploaded files
|
||||
stats <- asks serverStats
|
||||
liftIO $
|
||||
runExceptT (receiveFile getBody (XFTPRcvChunkSpec fPath size digest)) >>= \case
|
||||
Right () -> do
|
||||
used <- readTVarIO $ usedStorage st
|
||||
if maybe False (used + fromIntegral size >) quota_
|
||||
then remove fPath $> FRErr QUOTA
|
||||
else do
|
||||
atomically (setFilePath' st fr fPath)
|
||||
atomically $ modifyTVar' (filesUploaded stats) (+ 1)
|
||||
atomically $ modifyTVar' (filesCount stats) (+ 1)
|
||||
atomically $ modifyTVar' (filesSize stats) (+ fromIntegral size)
|
||||
pure FROk
|
||||
Left e -> remove fPath $> FRErr e
|
||||
-- TODO validate body size from request before downloading, once it's populated
|
||||
Just getBody -> skipCommitted $ ifM reserve receive (pure $ FRErr QUOTA)
|
||||
where
|
||||
remove fPath = whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
|
||||
|
||||
-- having a filePath means the file is already uploaded and committed, must not change anything
|
||||
skipCommitted = ifM (isJust <$> readTVarIO filePath) (liftIO $ drain $ fromIntegral size)
|
||||
where
|
||||
-- can't send FROk without reading the request body or a client will block on sending it
|
||||
-- can't send any old error as the client would fail or restart indefinitely
|
||||
drain s = do
|
||||
bs <- B.length <$> getBody fileBlockSize
|
||||
if
|
||||
| bs == s -> pure FROk
|
||||
| bs == 0 || bs > s -> pure $ FRErr SIZE
|
||||
| otherwise -> drain (s - bs)
|
||||
reserve = do
|
||||
us <- asks $ usedStorage . store
|
||||
quota <- asks $ fromMaybe maxBound . fileSizeQuota . config
|
||||
atomically . stateTVar us $
|
||||
\used -> let used' = used + fromIntegral size in if used' <= quota then (True, used') else (False, used)
|
||||
receive = do
|
||||
path <- asks $ filesPath . config
|
||||
let fPath = path </> B.unpack (B64.encode senderId)
|
||||
receiveChunk (XFTPRcvChunkSpec fPath size digest) >>= \case
|
||||
Right () -> do
|
||||
stats <- asks serverStats
|
||||
withFileLog $ \sl -> logPutFile sl senderId fPath
|
||||
atomically $ writeTVar filePath (Just fPath)
|
||||
atomically $ modifyTVar' (filesUploaded stats) (+ 1)
|
||||
atomically $ modifyTVar' (filesCount stats) (+ 1)
|
||||
atomically $ modifyTVar' (filesSize stats) (+ fromIntegral size)
|
||||
pure FROk
|
||||
Left e -> do
|
||||
us <- asks $ usedStorage . store
|
||||
atomically . modifyTVar' us $ subtract (fromIntegral size)
|
||||
liftIO $ whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
|
||||
pure $ FRErr e
|
||||
receiveChunk spec = do
|
||||
t <- asks $ fileTimeout . config
|
||||
liftIO $ fromMaybe (Left TIMEOUT) <$> timeout t (runExceptT (receiveFile getBody spec) `catchAll_` pure (Left FILE_IO))
|
||||
sendServerFile :: FileRec -> RcvPublicDhKey -> M (FileResponse, Maybe ServerFile)
|
||||
sendServerFile FileRec {senderId, filePath, fileInfo = FileInfo {size}} rDhKey = do
|
||||
readTVarIO filePath >>= \case
|
||||
Just path -> do
|
||||
g <- asks random
|
||||
(sDhKey, spDhKey) <- atomically $ C.generateKeyPair g
|
||||
let dhSecret = C.dh' rDhKey spDhKey
|
||||
cbNonce <- atomically $ C.randomCbNonce g
|
||||
case LC.cbInit dhSecret cbNonce of
|
||||
Right sbState -> do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
|
||||
atomically $ updatePeriodStats (filesDownloaded stats) senderId
|
||||
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
|
||||
_ -> pure (FRErr INTERNAL, Nothing)
|
||||
Just path -> ifM (doesFileExist path) sendFile (pure (FRErr AUTH, Nothing))
|
||||
where
|
||||
sendFile = do
|
||||
g <- asks random
|
||||
(sDhKey, spDhKey) <- atomically $ C.generateKeyPair g
|
||||
let dhSecret = C.dh' rDhKey spDhKey
|
||||
cbNonce <- atomically $ C.randomCbNonce g
|
||||
case LC.cbInit dhSecret cbNonce of
|
||||
Right sbState -> do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
|
||||
atomically $ updatePeriodStats (filesDownloaded stats) senderId
|
||||
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
|
||||
_ -> pure (FRErr INTERNAL, Nothing)
|
||||
_ -> pure (FRErr NO_FILE, Nothing)
|
||||
|
||||
deleteServerFile :: FileRec -> M FileResponse
|
||||
@@ -422,7 +502,34 @@ deleteServerFile_ FileRec {senderId, fileInfo, filePath} = do
|
||||
atomically $ modifyTVar' (filesCount stats) (subtract 1)
|
||||
atomically $ modifyTVar' (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
|
||||
|
||||
randomId :: (MonadUnliftIO m, MonadReader XFTPEnv m) => Int -> m ByteString
|
||||
expireServerFiles :: Maybe Int -> ExpirationConfig -> M ()
|
||||
expireServerFiles itemDelay expCfg = do
|
||||
st <- asks store
|
||||
usedStart <- readTVarIO $ usedStorage st
|
||||
old <- liftIO $ expireBeforeEpoch expCfg
|
||||
files' <- readTVarIO (files st)
|
||||
logInfo $ "Expiration check: " <> tshow (M.size files') <> " files"
|
||||
forM_ (M.keys files') $ \sId -> do
|
||||
mapM_ threadDelay itemDelay
|
||||
atomically (expiredFilePath st sId old)
|
||||
>>= mapM_ (maybeRemove $ delete st sId)
|
||||
usedEnd <- readTVarIO $ usedStorage st
|
||||
logInfo $ "Used " <> mbs usedStart <> " -> " <> mbs usedEnd <> ", " <> mbs (usedStart - usedEnd) <> " reclaimed."
|
||||
where
|
||||
mbs bs = tshow (bs `div` 1048576) <> "mb"
|
||||
maybeRemove del = maybe del (remove del)
|
||||
remove del filePath =
|
||||
ifM
|
||||
(doesFileExist filePath)
|
||||
((removeFile filePath >> del) `catch` \(e :: SomeException) -> logError $ "failed to remove expired file " <> tshow filePath <> ": " <> tshow e)
|
||||
del
|
||||
delete st sId = do
|
||||
withFileLog (`logDeleteFile` sId)
|
||||
void . atomically $ deleteFile st sId -- will not update usedStorage if sId isn't in store
|
||||
FileServerStats {filesExpired} <- asks serverStats
|
||||
atomically $ modifyTVar' filesExpired (+ 1)
|
||||
|
||||
randomId :: Int -> M ByteString
|
||||
randomId n = atomically . C.randomBytes n =<< asks random
|
||||
|
||||
getFileId :: M XFTPFileId
|
||||
@@ -430,7 +537,7 @@ getFileId = do
|
||||
size <- asks (fileIdSize . config)
|
||||
atomically . C.randomBytes size =<< asks random
|
||||
|
||||
withFileLog :: (MonadIO m, MonadReader XFTPEnv m) => (StoreLog 'WriteMode -> IO a) -> m ()
|
||||
withFileLog :: (StoreLog 'WriteMode -> IO a) -> M ()
|
||||
withFileLog action = liftIO . mapM_ action =<< asks storeLog
|
||||
|
||||
incFileStat :: (FileServerStats -> TVar Int) -> M ()
|
||||
|
||||
@@ -6,9 +6,13 @@ module Simplex.FileTransfer.Server.Control where
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString (ByteString)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth)
|
||||
|
||||
data CPClientRole = CPRNone | CPRUser | CPRAdmin
|
||||
|
||||
data ControlProtocol
|
||||
= CPStatsRTS
|
||||
= CPAuth BasicAuth
|
||||
| CPStatsRTS
|
||||
| CPDelete ByteString
|
||||
| CPHelp
|
||||
| CPQuit
|
||||
@@ -16,15 +20,17 @@ data ControlProtocol
|
||||
|
||||
instance StrEncoding ControlProtocol where
|
||||
strEncode = \case
|
||||
CPAuth tok -> "auth " <> strEncode tok
|
||||
CPStatsRTS -> "stats-rts"
|
||||
CPDelete bs -> "delete " <> strEncode bs
|
||||
CPDelete fId -> strEncode (Str "delete", fId)
|
||||
CPHelp -> "help"
|
||||
CPQuit -> "quit"
|
||||
CPSkip -> ""
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"auth" -> CPAuth <$> _strP
|
||||
"stats-rts" -> pure CPStatsRTS
|
||||
"delete" -> CPDelete <$> (A.space *> strP)
|
||||
"delete" -> CPDelete <$> _strP
|
||||
"help" -> pure CPHelp
|
||||
"quit" -> pure CPQuit
|
||||
"" -> pure CPSkip
|
||||
|
||||
@@ -2,31 +2,37 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
|
||||
module Simplex.FileTransfer.Server.Env where
|
||||
|
||||
import Control.Logger.Simple (logInfo)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.Default (def)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Word (Word32)
|
||||
import Data.X509.Validation (Fingerprint (..))
|
||||
import Network.Socket
|
||||
import qualified Network.TLS as T
|
||||
import Simplex.FileTransfer.Protocol (FileCmd, FileInfo, XFTPFileId)
|
||||
import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId)
|
||||
import Simplex.FileTransfer.Server.Stats
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.FileTransfer.Server.StoreLog
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Transport (ALPN)
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import System.IO (IOMode (..))
|
||||
import UnliftIO.STM
|
||||
@@ -45,8 +51,13 @@ data XFTPServerConfig = XFTPServerConfig
|
||||
allowNewFiles :: Bool,
|
||||
-- | simple password that the clients need to pass in handshake to be able to create new files
|
||||
newFileBasicAuth :: Maybe BasicAuth,
|
||||
-- | control port passwords,
|
||||
controlPortUserAuth :: Maybe BasicAuth,
|
||||
controlPortAdminAuth :: Maybe BasicAuth,
|
||||
-- | time after which the files can be removed and check interval, seconds
|
||||
fileExpiration :: Maybe ExpirationConfig,
|
||||
-- | timeout to receive file
|
||||
fileTimeout :: Int,
|
||||
-- | time after which inactive clients can be disconnected and check interval, seconds
|
||||
inactiveClientExpiration :: Maybe ExpirationConfig,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
@@ -88,20 +99,37 @@ defaultFileExpiration =
|
||||
checkInterval = 2 * 3600 -- seconds, 2 hours
|
||||
}
|
||||
|
||||
newXFTPServerEnv :: (MonadUnliftIO m, MonadRandom m) => XFTPServerConfig -> m XFTPEnv
|
||||
supportedXFTPhandshakes :: [ALPN]
|
||||
supportedXFTPhandshakes = ["xftp/1"]
|
||||
|
||||
newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv
|
||||
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile} = do
|
||||
random <- liftIO C.newRandom
|
||||
store <- atomically newFileStore
|
||||
storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile
|
||||
used <- readTVarIO (usedStorage store)
|
||||
used <- countUsedStorage <$> readTVarIO (files store)
|
||||
atomically $ writeTVar (usedStorage store) used
|
||||
forM_ fileSizeQuota $ \quota -> do
|
||||
logInfo $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used)
|
||||
when (quota < used) $ logInfo "WARNING: storage quota is less than used storage, no files can be uploaded!"
|
||||
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
tlsServerParams' <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
let TransportServerConfig {alpn} = transportConfig config
|
||||
let tlsServerParams = case alpn of
|
||||
Nothing -> tlsServerParams'
|
||||
Just supported ->
|
||||
tlsServerParams'
|
||||
{ T.serverHooks =
|
||||
def
|
||||
{ T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported)
|
||||
}
|
||||
}
|
||||
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
|
||||
serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime
|
||||
pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
|
||||
|
||||
countUsedStorage :: M.Map k FileRec -> Int64
|
||||
countUsedStorage = M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0
|
||||
|
||||
data XFTPRequest
|
||||
= XFTPReqNew FileInfo (NonEmpty RcvPublicAuthKey) (Maybe BasicAuth)
|
||||
| XFTPReqCmd XFTPFileId FileRec FileCmd
|
||||
|
||||
@@ -97,6 +97,8 @@ xftpServerCLI cfgPath logPath = do
|
||||
\# with the users who you want to allow uploading files to your server.\n\
|
||||
\# create_password: password to upload files (any printable ASCII characters without whitespace, '@', ':' and '/')\n\
|
||||
\\n\
|
||||
\# control_port_admin_password:\n\
|
||||
\# control_port_user_password:\n\
|
||||
\[TRANSPORT]\n\
|
||||
\# host is only used to print server address on start\n"
|
||||
<> ("host: " <> host <> "\n")
|
||||
@@ -155,11 +157,14 @@ xftpServerCLI cfgPath logPath = do
|
||||
allowedChunkSizes = serverChunkSizes,
|
||||
allowNewFiles = fromMaybe True $ iniOnOff "AUTH" "new_files" ini,
|
||||
newFileBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
|
||||
controlPortAdminAuth = either error id <$> strDecodeIni "AUTH" "control_port_admin_password" ini,
|
||||
controlPortUserAuth = either error id <$> strDecodeIni "AUTH" "control_port_user_password" ini,
|
||||
fileExpiration =
|
||||
Just
|
||||
defaultFileExpiration
|
||||
{ ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini
|
||||
},
|
||||
fileTimeout = 10 * 60 * 1000000, -- 10 mins to send 4mb chunk
|
||||
inactiveClientExpiration =
|
||||
settingIsOn "INACTIVE_CLIENTS" "disconnect" ini
|
||||
$> ExpirationConfig
|
||||
|
||||
@@ -11,7 +11,6 @@ module Simplex.FileTransfer.Server.Store
|
||||
newFileStore,
|
||||
addFile,
|
||||
setFilePath,
|
||||
setFilePath',
|
||||
addRecipient,
|
||||
deleteFile,
|
||||
deleteRecipient,
|
||||
@@ -23,7 +22,6 @@ where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
@@ -79,12 +77,10 @@ newFileRec senderId fileInfo createdAt = do
|
||||
|
||||
setFilePath :: FileStore -> SenderId -> FilePath -> STM (Either XFTPErrorType ())
|
||||
setFilePath st sId fPath =
|
||||
withFile st sId $ \fr -> setFilePath' st fr fPath $> Right ()
|
||||
|
||||
setFilePath' :: FileStore -> FileRec -> FilePath -> STM ()
|
||||
setFilePath' st FileRec {fileInfo, filePath} fPath = do
|
||||
writeTVar filePath (Just fPath)
|
||||
modifyTVar' (usedStorage st) (+ fromIntegral (size fileInfo))
|
||||
withFile st sId $ \FileRec {fileInfo, filePath} -> do
|
||||
writeTVar filePath (Just fPath)
|
||||
modifyTVar' (usedStorage st) (+ fromIntegral (size fileInfo))
|
||||
pure $ Right ()
|
||||
|
||||
addRecipient :: FileStore -> SenderId -> FileRecipient -> STM (Either XFTPErrorType ())
|
||||
addRecipient st@FileStore {recipients} senderId (FileRecipient rId rKey) =
|
||||
|
||||
@@ -22,6 +22,7 @@ import Control.Concurrent.STM
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Composition ((.:), (.:.))
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
@@ -88,7 +89,7 @@ readWriteFileStore f st = do
|
||||
pure s
|
||||
|
||||
readFileStore :: FilePath -> FileStore -> IO ()
|
||||
readFileStore f st = mapM_ addFileLogRecord . B.lines =<< B.readFile f
|
||||
readFileStore f st = mapM_ (addFileLogRecord . LB.toStrict) . LB.lines =<< LB.readFile f
|
||||
where
|
||||
addFileLogRecord s = case strDecode s of
|
||||
Left e -> B.putStrLn $ "Log parsing error (" <> B.pack e <> "): " <> B.take 100 s
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
@@ -9,9 +10,17 @@
|
||||
|
||||
module Simplex.FileTransfer.Transport
|
||||
( supportedFileServerVRange,
|
||||
xftpClientHandshake, -- stub
|
||||
XFTPVersion,
|
||||
authCmdsXFTPVersion,
|
||||
xftpClientHandshakeStub,
|
||||
XFTPClientHandshake (..),
|
||||
-- xftpClientHandshake,
|
||||
XFTPServerHandshake (..),
|
||||
-- xftpServerHandshake,
|
||||
THandleXFTP,
|
||||
THandleParamsXFTP,
|
||||
VersionXFTP,
|
||||
VersionRangeXFTP,
|
||||
XFTPVersion,
|
||||
pattern VersionXFTP,
|
||||
XFTPErrorType (..),
|
||||
XFTPRcvChunkSpec (..),
|
||||
@@ -30,20 +39,21 @@ import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Word (Word16, Word32)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol (CommandError)
|
||||
import Simplex.Messaging.Transport (HandshakeError (..), THandle, TransportError (..))
|
||||
import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import Simplex.Messaging.Version
|
||||
@@ -68,15 +78,58 @@ type VersionRangeXFTP = VersionRange XFTPVersion
|
||||
pattern VersionXFTP :: Word16 -> VersionXFTP
|
||||
pattern VersionXFTP v = Version v
|
||||
|
||||
type THandleXFTP c p = THandle XFTPVersion c p
|
||||
type THandleParamsXFTP p = THandleParams XFTPVersion p
|
||||
|
||||
initialXFTPVersion :: VersionXFTP
|
||||
initialXFTPVersion = VersionXFTP 1
|
||||
|
||||
supportedFileServerVRange :: VersionRangeXFTP
|
||||
supportedFileServerVRange = mkVersionRange initialXFTPVersion initialXFTPVersion
|
||||
authCmdsXFTPVersion :: VersionXFTP
|
||||
authCmdsXFTPVersion = VersionXFTP 2
|
||||
|
||||
-- XFTP protocol does not support handshake
|
||||
xftpClientHandshake :: c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c)
|
||||
xftpClientHandshake _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION
|
||||
currentXFTPVersion :: VersionXFTP
|
||||
currentXFTPVersion = VersionXFTP 2
|
||||
|
||||
supportedFileServerVRange :: VersionRangeXFTP
|
||||
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
|
||||
|
||||
data XFTPServerHandshake = XFTPServerHandshake
|
||||
{ xftpVersionRange :: VersionRangeXFTP,
|
||||
sessionId :: SessionId,
|
||||
-- | pub key to agree shared secrets for command authorization and entity ID encryption.
|
||||
authPubKey :: (X.CertificateChain, X.SignedExact X.PubKey)
|
||||
}
|
||||
|
||||
data XFTPClientHandshake = XFTPClientHandshake
|
||||
{ -- | agreed XFTP server protocol version
|
||||
xftpVersion :: VersionXFTP,
|
||||
-- | server identity - CA certificate fingerprint
|
||||
keyHash :: C.KeyHash
|
||||
}
|
||||
|
||||
instance Encoding XFTPClientHandshake where
|
||||
smpEncode XFTPClientHandshake {xftpVersion, keyHash} =
|
||||
smpEncode (xftpVersion, keyHash)
|
||||
smpP = do
|
||||
(xftpVersion, keyHash) <- smpP
|
||||
Tail _compat <- smpP
|
||||
pure XFTPClientHandshake {xftpVersion, keyHash}
|
||||
|
||||
instance Encoding XFTPServerHandshake where
|
||||
smpEncode XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey} =
|
||||
smpEncode (xftpVersionRange, sessionId, auth)
|
||||
where
|
||||
auth = bimap C.encodeCertChain C.SignedObject authPubKey
|
||||
smpP = do
|
||||
(xftpVersionRange, sessionId) <- smpP
|
||||
cert <- C.certChainP
|
||||
C.SignedObject key <- smpP
|
||||
Tail _compat <- smpP
|
||||
pure XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey = (cert, key)}
|
||||
|
||||
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
|
||||
sendEncFile h send = go
|
||||
@@ -139,6 +192,8 @@ data XFTPErrorType
|
||||
BLOCK
|
||||
| -- | incorrect SMP session ID (TLS Finished message / tls-unique binding RFC5929)
|
||||
SESSION
|
||||
| -- | incorrect handshake command
|
||||
HANDSHAKE
|
||||
| -- | SMP command is unknown or has invalid syntax
|
||||
CMD {cmdErr :: CommandError}
|
||||
| -- | command authorization error - bad signature or non-existing SMP queue
|
||||
@@ -157,6 +212,8 @@ data XFTPErrorType
|
||||
HAS_FILE
|
||||
| -- | file IO error
|
||||
FILE_IO
|
||||
| -- | file sending timeout
|
||||
TIMEOUT
|
||||
| -- | bad redirect data
|
||||
REDIRECT {redirectError :: String}
|
||||
| -- | internal server error
|
||||
@@ -179,6 +236,7 @@ instance Encoding XFTPErrorType where
|
||||
smpEncode = \case
|
||||
BLOCK -> "BLOCK"
|
||||
SESSION -> "SESSION"
|
||||
HANDSHAKE -> "HANDSHAKE"
|
||||
CMD err -> "CMD " <> smpEncode err
|
||||
AUTH -> "AUTH"
|
||||
SIZE -> "SIZE"
|
||||
@@ -188,6 +246,7 @@ instance Encoding XFTPErrorType where
|
||||
NO_FILE -> "NO_FILE"
|
||||
HAS_FILE -> "HAS_FILE"
|
||||
FILE_IO -> "FILE_IO"
|
||||
TIMEOUT -> "TIMEOUT"
|
||||
REDIRECT err -> "REDIRECT " <> smpEncode err
|
||||
INTERNAL -> "INTERNAL"
|
||||
DUPLICATE_ -> "DUPLICATE_"
|
||||
@@ -196,6 +255,7 @@ instance Encoding XFTPErrorType where
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"BLOCK" -> pure BLOCK
|
||||
"SESSION" -> pure SESSION
|
||||
"HANDSHAKE" -> pure HANDSHAKE
|
||||
"CMD" -> CMD <$> _smpP
|
||||
"AUTH" -> pure AUTH
|
||||
"SIZE" -> pure SIZE
|
||||
@@ -205,6 +265,7 @@ instance Encoding XFTPErrorType where
|
||||
"NO_FILE" -> pure NO_FILE
|
||||
"HAS_FILE" -> pure HAS_FILE
|
||||
"FILE_IO" -> pure FILE_IO
|
||||
"TIMEOUT" -> pure TIMEOUT
|
||||
"REDIRECT" -> REDIRECT <$> _smpP
|
||||
"INTERNAL" -> pure INTERNAL
|
||||
"DUPLICATE_" -> pure DUPLICATE_
|
||||
|
||||
+475
-387
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Env.SQLite
|
||||
( AgentMonad,
|
||||
AgentMonad',
|
||||
( AM',
|
||||
AM,
|
||||
AgentConfig (..),
|
||||
InitialAgentServers (..),
|
||||
NetworkConfig (..),
|
||||
@@ -21,6 +21,7 @@ module Simplex.Messaging.Agent.Env.SQLite
|
||||
tryAgentError,
|
||||
tryAgentError',
|
||||
catchAgentError,
|
||||
catchAgentError',
|
||||
agentFinally,
|
||||
Env (..),
|
||||
newSMPAgentEnv,
|
||||
@@ -34,7 +35,6 @@ module Simplex.Messaging.Agent.Env.SQLite
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
@@ -65,14 +65,14 @@ import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..))
|
||||
import Simplex.Messaging.Transport.Client (defaultSMPPort)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors')
|
||||
import System.Random (StdGen, newStdGen)
|
||||
import UnliftIO (Async, SomeException)
|
||||
import UnliftIO.STM
|
||||
|
||||
type AgentMonad' m = (MonadUnliftIO m, MonadReader Env m)
|
||||
type AM' a = ReaderT Env IO a
|
||||
|
||||
type AgentMonad m = (AgentMonad' m, MonadError AgentErrorType m)
|
||||
type AM a = ExceptT AgentErrorType (ReaderT Env IO) a
|
||||
|
||||
data InitialAgentServers = InitialAgentServers
|
||||
{ smp :: Map UserId (NonEmpty SMPServerWithAuth),
|
||||
@@ -82,7 +82,7 @@ data InitialAgentServers = InitialAgentServers
|
||||
}
|
||||
|
||||
data AgentConfig = AgentConfig
|
||||
{ tcpPort :: ServiceName,
|
||||
{ tcpPort :: Maybe ServiceName,
|
||||
rcvAuthAlg :: C.AuthAlg,
|
||||
sndAuthAlg :: C.AuthAlg,
|
||||
connIdBytes :: Int,
|
||||
@@ -92,6 +92,7 @@ data AgentConfig = AgentConfig
|
||||
xftpCfg :: XFTPClientConfig,
|
||||
reconnectInterval :: RetryInterval,
|
||||
messageRetryInterval :: RetryInterval2,
|
||||
userNetworkInterval :: RetryInterval,
|
||||
messageTimeout :: NominalDiffTime,
|
||||
connDeleteDeliveryTimeout :: NominalDiffTime,
|
||||
helloTimeout :: NominalDiffTime,
|
||||
@@ -126,7 +127,7 @@ defaultReconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 2_000000,
|
||||
increaseAfter = 10_000000,
|
||||
maxInterval = 180_000000
|
||||
maxInterval = 60_000000
|
||||
}
|
||||
|
||||
defaultMessageRetryInterval :: RetryInterval2
|
||||
@@ -134,22 +135,30 @@ defaultMessageRetryInterval =
|
||||
RetryInterval2
|
||||
{ riFast =
|
||||
RetryInterval
|
||||
{ initialInterval = 1_000000,
|
||||
{ initialInterval = 2_000000,
|
||||
increaseAfter = 10_000000,
|
||||
maxInterval = 60_000000
|
||||
},
|
||||
riSlow =
|
||||
RetryInterval
|
||||
{ initialInterval = 180_000000, -- 3 minutes
|
||||
{ initialInterval = 300_000000, -- 5 minutes
|
||||
increaseAfter = 60_000000,
|
||||
maxInterval = 3 * 3600_000000 -- 3 hours
|
||||
maxInterval = 6 * 3600_000000 -- 6 hours
|
||||
}
|
||||
}
|
||||
|
||||
defaultUserNetworkInterval :: RetryInterval
|
||||
defaultUserNetworkInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 1200_000000, -- 20 minutes
|
||||
increaseAfter = 0,
|
||||
maxInterval = 7200_000000 -- 2 hours
|
||||
}
|
||||
|
||||
defaultAgentConfig :: AgentConfig
|
||||
defaultAgentConfig =
|
||||
AgentConfig
|
||||
{ tcpPort = "5224",
|
||||
{ tcpPort = Just "5224",
|
||||
-- while the current client version supports X25519, it can only be enabled once support for SMP v6 is dropped,
|
||||
-- and all servers are required to support v7 to be compatible.
|
||||
rcvAuthAlg = C.AuthAlg C.SEd25519, -- this will stay as Ed25519
|
||||
@@ -161,6 +170,7 @@ defaultAgentConfig =
|
||||
xftpCfg = defaultXFTPClientConfig,
|
||||
reconnectInterval = defaultReconnectInterval,
|
||||
messageRetryInterval = defaultMessageRetryInterval,
|
||||
userNetworkInterval = defaultUserNetworkInterval,
|
||||
messageTimeout = 2 * nominalDay,
|
||||
connDeleteDeliveryTimeout = 2 * nominalDay,
|
||||
helloTimeout = 2 * nominalDay,
|
||||
@@ -250,20 +260,24 @@ newXFTPAgent = do
|
||||
xftpDelWorkers <- TM.empty
|
||||
pure XFTPAgent {xftpWorkDir, xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}
|
||||
|
||||
tryAgentError :: AgentMonad m => m a -> m (Either AgentErrorType a)
|
||||
tryAgentError :: AM a -> AM (Either AgentErrorType a)
|
||||
tryAgentError = tryAllErrors mkInternal
|
||||
{-# INLINE tryAgentError #-}
|
||||
|
||||
-- unlike runExceptT, this ensures we catch IO exceptions as well
|
||||
tryAgentError' :: AgentMonad' m => ExceptT AgentErrorType m a -> m (Either AgentErrorType a)
|
||||
tryAgentError' = fmap join . runExceptT . tryAgentError
|
||||
tryAgentError' :: AM a -> AM' (Either AgentErrorType a)
|
||||
tryAgentError' = tryAllErrors' mkInternal
|
||||
{-# INLINE tryAgentError' #-}
|
||||
|
||||
catchAgentError :: AgentMonad m => m a -> (AgentErrorType -> m a) -> m a
|
||||
catchAgentError :: AM a -> (AgentErrorType -> AM a) -> AM a
|
||||
catchAgentError = catchAllErrors mkInternal
|
||||
{-# INLINE catchAgentError #-}
|
||||
|
||||
agentFinally :: AgentMonad m => m a -> m b -> m a
|
||||
catchAgentError' :: AM a -> (AgentErrorType -> AM' a) -> AM' a
|
||||
catchAgentError' = catchAllErrors' mkInternal
|
||||
{-# INLINE catchAgentError' #-}
|
||||
|
||||
agentFinally :: AM a -> AM b -> AM a
|
||||
agentFinally = allFinally mkInternal
|
||||
{-# INLINE agentFinally #-}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Lock
|
||||
( Lock,
|
||||
createLock,
|
||||
withLock,
|
||||
withLock',
|
||||
withGetLock,
|
||||
withGetLocks,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.Except (ExceptT (..), runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Functor (($>))
|
||||
import UnliftIO.Async (forConcurrently)
|
||||
@@ -22,8 +22,12 @@ createLock :: STM Lock
|
||||
createLock = newEmptyTMVar
|
||||
{-# INLINE createLock #-}
|
||||
|
||||
withLock :: MonadUnliftIO m => Lock -> String -> m a -> m a
|
||||
withLock lock name =
|
||||
withLock :: MonadUnliftIO m => Lock -> String -> ExceptT e m a -> ExceptT e m a
|
||||
withLock lock name = ExceptT . withLock' lock name . runExceptT
|
||||
{-# INLINE withLock #-}
|
||||
|
||||
withLock' :: MonadUnliftIO m => Lock -> String -> m a -> m a
|
||||
withLock' lock name =
|
||||
E.bracket_
|
||||
(atomically $ putTMVar lock name)
|
||||
(void . atomically $ takeTMVar lock)
|
||||
|
||||
@@ -44,7 +44,7 @@ import UnliftIO
|
||||
import UnliftIO.Concurrent (forkIO, threadDelay)
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
runNtfSupervisor :: forall m. AgentMonad' m => AgentClient -> m ()
|
||||
runNtfSupervisor :: AgentClient -> AM' ()
|
||||
runNtfSupervisor c = do
|
||||
ns <- asks ntfSupervisor
|
||||
forever $ do
|
||||
@@ -54,13 +54,13 @@ runNtfSupervisor c = do
|
||||
Left e -> notifyErr connId e
|
||||
Right _ -> return ()
|
||||
where
|
||||
handleErr :: ConnId -> m () -> m ()
|
||||
handleErr :: ConnId -> AM' () -> AM' ()
|
||||
handleErr connId = E.handle $ \(e :: E.SomeException) -> do
|
||||
logError $ "runNtfSupervisor error " <> tshow e
|
||||
notifyErr connId e
|
||||
notifyErr connId e = notifyInternalError c connId $ "runNtfSupervisor error " <> show e
|
||||
|
||||
processNtfSub :: forall m. AgentMonad m => AgentClient -> (ConnId, NtfSupervisorCommand) -> m ()
|
||||
processNtfSub :: AgentClient -> (ConnId, NtfSupervisorCommand) -> AM ()
|
||||
processNtfSub c (connId, cmd) = do
|
||||
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
|
||||
case cmd of
|
||||
@@ -77,11 +77,11 @@ processNtfSub c (connId, cmd) = do
|
||||
Just ClientNtfCreds {notifierId} -> do
|
||||
let newSub = newNtfSubscription connId smpServer (Just notifierId) ntfServer NASKey
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubNTFAction NSACreate
|
||||
void $ getNtfNTFWorker True c ntfServer
|
||||
lift . void $ getNtfNTFWorker True c ntfServer
|
||||
Nothing -> do
|
||||
let newSub = newNtfSubscription connId smpServer Nothing ntfServer NASNew
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubSMPAction NSASmpKey
|
||||
void $ getNtfSMPWorker True c smpServer
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
(Just (sub@NtfSubscription {ntfSubStatus, ntfServer = subNtfServer, smpServer = smpServer', ntfQueueId}, action_)) -> do
|
||||
case (clientNtfCreds, ntfQueueId) of
|
||||
(Just ClientNtfCreds {notifierId}, Just ntfQueueId')
|
||||
@@ -90,7 +90,7 @@ processNtfSub c (connId, cmd) = do
|
||||
(Nothing, Nothing) -> create
|
||||
_ -> rotate
|
||||
where
|
||||
create :: m ()
|
||||
create :: AM ()
|
||||
create = case action_ of
|
||||
-- action was set to NULL after worker internal error
|
||||
Nothing -> resetSubscription
|
||||
@@ -101,75 +101,76 @@ processNtfSub c (connId, cmd) = do
|
||||
then resetSubscription
|
||||
else withTokenServer $ \ntfServer -> do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
|
||||
void $ getNtfNTFWorker True c ntfServer
|
||||
lift . void $ getNtfNTFWorker True c ntfServer
|
||||
| otherwise -> case action of
|
||||
NtfSubNTFAction _ -> void $ getNtfNTFWorker True c subNtfServer
|
||||
NtfSubSMPAction _ -> void $ getNtfSMPWorker True c smpServer
|
||||
rotate :: m ()
|
||||
NtfSubNTFAction _ -> lift . void $ getNtfNTFWorker True c subNtfServer
|
||||
NtfSubSMPAction _ -> lift . void $ getNtfSMPWorker True c smpServer
|
||||
rotate :: AM ()
|
||||
rotate = do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NtfSubNTFAction NSARotate)
|
||||
void $ getNtfNTFWorker True c subNtfServer
|
||||
resetSubscription :: m ()
|
||||
lift . void $ getNtfNTFWorker True c subNtfServer
|
||||
resetSubscription :: AM ()
|
||||
resetSubscription =
|
||||
withTokenServer $ \ntfServer -> do
|
||||
let sub' = sub {ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub' (NtfSubSMPAction NSASmpKey)
|
||||
void $ getNtfSMPWorker True c smpServer
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
NSCDelete -> do
|
||||
sub_ <- withStore' c $ \db -> do
|
||||
supervisorUpdateNtfAction db connId (NtfSubNTFAction NSADelete)
|
||||
getNtfSubscription db connId
|
||||
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
|
||||
case sub_ of
|
||||
(Just (NtfSubscription {ntfServer}, _)) -> void $ getNtfNTFWorker True c ntfServer
|
||||
(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
|
||||
withStore' c $ \db -> supervisorUpdateNtfAction db connId (NtfSubSMPAction NSASmpDelete)
|
||||
void $ getNtfSMPWorker True c smpServer
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
|
||||
NSCNtfWorker ntfServer -> void $ getNtfNTFWorker True c ntfServer
|
||||
NSCNtfSMPWorker smpServer -> void $ getNtfSMPWorker True c smpServer
|
||||
NSCNtfWorker ntfServer -> lift . void $ getNtfNTFWorker True c ntfServer
|
||||
NSCNtfSMPWorker smpServer -> lift . void $ getNtfSMPWorker True c smpServer
|
||||
|
||||
getNtfNTFWorker :: AgentMonad' m => Bool -> AgentClient -> NtfServer -> m Worker
|
||||
getNtfNTFWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
|
||||
getNtfNTFWorker hasWork c server = do
|
||||
ws <- asks $ ntfWorkers . ntfSupervisor
|
||||
getAgentWorker "ntf_ntf" hasWork c server ws $ runNtfWorker c server
|
||||
|
||||
getNtfSMPWorker :: AgentMonad' m => Bool -> AgentClient -> SMPServer -> m Worker
|
||||
getNtfSMPWorker :: Bool -> AgentClient -> SMPServer -> AM' Worker
|
||||
getNtfSMPWorker hasWork c server = do
|
||||
ws <- asks $ ntfSMPWorkers . ntfSupervisor
|
||||
getAgentWorker "ntf_smp" hasWork c server ws $ runNtfSMPWorker c server
|
||||
|
||||
withTokenServer :: AgentMonad' m => (NtfServer -> m ()) -> m ()
|
||||
withTokenServer action = getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
|
||||
withTokenServer :: (NtfServer -> AM ()) -> AM ()
|
||||
withTokenServer action = lift getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
|
||||
|
||||
runNtfWorker :: forall m. AgentMonad m => AgentClient -> NtfServer -> Worker -> m ()
|
||||
runNtfWorker :: AgentClient -> NtfServer -> Worker -> AM ()
|
||||
runNtfWorker c srv Worker {doWork} = do
|
||||
delay <- asks $ ntfWorkerDelay . config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
agentOperationBracket c AONtfNetwork throwWhenInactive runNtfOperation
|
||||
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
|
||||
threadDelay delay
|
||||
where
|
||||
runNtfOperation :: m ()
|
||||
runNtfOperation :: AM ()
|
||||
runNtfOperation =
|
||||
withWork c doWork (`getNextNtfSubNTFAction` srv) $
|
||||
\nextSub@(NtfSubscription {connId}, _, _) -> do
|
||||
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop ->
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> m ()
|
||||
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
|
||||
processSub (sub@NtfSubscription {connId, smpServer, ntfSubId}, action, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (rescheduleAction doWork ts actionTs) $
|
||||
unlessM (lift $ rescheduleAction doWork ts actionTs) $
|
||||
case action of
|
||||
NSACreate ->
|
||||
getNtfToken >>= \case
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
RcvQueue {clientNtfCreds} <- withStore c (`getPrimaryRcvQueue` connId)
|
||||
case clientNtfCreds of
|
||||
@@ -182,13 +183,13 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
_ -> workerInternalError c connId "NSACreate - no notifier queue credentials"
|
||||
_ -> workerInternalError c connId "NSACreate - no active token"
|
||||
NSACheck ->
|
||||
getNtfToken >>= \case
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn ->
|
||||
case ntfSubId of
|
||||
Just nSubId ->
|
||||
agentNtfCheckSubscription c nSubId tkn >>= \case
|
||||
NSAuth -> do
|
||||
getNtfServer c >>= \case
|
||||
lift (getNtfServer c) >>= \case
|
||||
Just ntfServer -> do
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NtfSubSMPAction NSASmpKey) ts
|
||||
@@ -200,7 +201,7 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
_ -> workerInternalError c connId "NSACheck - no active token"
|
||||
NSADelete -> case ntfSubId of
|
||||
Just nSubId ->
|
||||
(getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
|
||||
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
|
||||
`agentFinally` continueDeletion
|
||||
_ -> continueDeletion
|
||||
where
|
||||
@@ -211,7 +212,7 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
NSARotate -> case ntfSubId of
|
||||
Just nSubId ->
|
||||
(getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
|
||||
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
|
||||
`agentFinally` deleteCreate
|
||||
_ -> deleteCreate
|
||||
where
|
||||
@@ -228,12 +229,14 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
|
||||
|
||||
runNtfSMPWorker :: forall m. AgentMonad m => AgentClient -> SMPServer -> Worker -> m ()
|
||||
runNtfSMPWorker :: AgentClient -> SMPServer -> Worker -> AM ()
|
||||
runNtfSMPWorker c srv Worker {doWork} = do
|
||||
env <- ask
|
||||
delay <- asks $ ntfSMPWorkerDelay . config
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
agentOperationBracket c AONtfNetwork throwWhenInactive runNtfSMPOperation
|
||||
ExceptT . liftIO . agentOperationBracket c AONtfNetwork throwWhenInactive $
|
||||
runReaderT (runExceptT runNtfSMPOperation) env
|
||||
threadDelay delay
|
||||
where
|
||||
runNtfSMPOperation =
|
||||
@@ -241,16 +244,17 @@ runNtfSMPWorker c srv Worker {doWork} = do
|
||||
\nextSub@(NtfSubscription {connId}, _, _) -> do
|
||||
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop ->
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
lift $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> m ()
|
||||
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM ()
|
||||
processSub (sub@NtfSubscription {connId, ntfServer}, smpAction, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (rescheduleAction doWork ts actionTs) $
|
||||
unlessM (lift $ rescheduleAction doWork ts actionTs) $
|
||||
case smpAction of
|
||||
NSASmpKey ->
|
||||
getNtfToken >>= \case
|
||||
lift getNtfToken >>= \case
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
rq <- withStore c (`getPrimaryRcvQueue` connId)
|
||||
C.AuthAlg a <- asks (rcvAuthAlg . config)
|
||||
@@ -272,7 +276,7 @@ runNtfSMPWorker c srv Worker {doWork} = do
|
||||
mapM_ (disableQueueNotifications c) rq_
|
||||
withStore' c $ \db -> deleteNtfSubscription db connId
|
||||
|
||||
rescheduleAction :: AgentMonad' m => TMVar () -> UTCTime -> UTCTime -> m Bool
|
||||
rescheduleAction :: TMVar () -> UTCTime -> UTCTime -> AM' Bool
|
||||
rescheduleAction doWork ts actionTs
|
||||
| actionTs <= ts = pure False
|
||||
| otherwise = do
|
||||
@@ -282,7 +286,7 @@ rescheduleAction doWork ts actionTs
|
||||
atomically $ hasWorkToDo' doWork
|
||||
pure True
|
||||
|
||||
retryOnError :: AgentMonad' m => AgentClient -> Text -> m () -> (AgentErrorType -> m ()) -> AgentErrorType -> m ()
|
||||
retryOnError :: AgentClient -> Text -> AM () -> (AgentErrorType -> AM ()) -> AgentErrorType -> AM ()
|
||||
retryOnError c name loop done e = do
|
||||
logError $ name <> " error: " <> tshow e
|
||||
case e of
|
||||
@@ -296,16 +300,17 @@ retryOnError c name loop done e = do
|
||||
atomically $ beginAgentOperation c AONtfNetwork
|
||||
loop
|
||||
|
||||
workerInternalError :: AgentMonad m => AgentClient -> ConnId -> String -> m ()
|
||||
workerInternalError :: AgentClient -> ConnId -> String -> AM ()
|
||||
workerInternalError c connId internalErrStr = do
|
||||
withStore' c $ \db -> setNullNtfSubscriptionAction db connId
|
||||
notifyInternalError c connId internalErrStr
|
||||
|
||||
-- TODO change error
|
||||
notifyInternalError :: MonadUnliftIO m => AgentClient -> ConnId -> String -> m ()
|
||||
notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m ()
|
||||
notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, APC SAEConn $ ERR $ INTERNAL internalErrStr)
|
||||
{-# INLINE notifyInternalError #-}
|
||||
|
||||
getNtfToken :: AgentMonad' m => m (Maybe NtfToken)
|
||||
getNtfToken :: AM' (Maybe NtfToken)
|
||||
getNtfToken = do
|
||||
tkn <- asks $ ntfTkn . ntfSupervisor
|
||||
readTVarIO tkn
|
||||
@@ -326,14 +331,14 @@ instantNotifications = \case
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> True
|
||||
_ -> False
|
||||
|
||||
closeNtfSupervisor :: MonadUnliftIO m => NtfSupervisor -> m ()
|
||||
closeNtfSupervisor :: NtfSupervisor -> IO ()
|
||||
closeNtfSupervisor ns = do
|
||||
stopWorkers $ ntfWorkers ns
|
||||
stopWorkers $ ntfSMPWorkers ns
|
||||
where
|
||||
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
|
||||
|
||||
getNtfServer :: AgentMonad' m => AgentClient -> m (Maybe NtfServer)
|
||||
getNtfServer :: AgentClient -> AM' (Maybe NtfServer)
|
||||
getNtfServer c = do
|
||||
ntfServers <- readTVarIO $ ntfServers c
|
||||
case ntfServers of
|
||||
|
||||
@@ -405,6 +405,7 @@ data ACommand (p :: AParty) (e :: AEntity) where
|
||||
MSGNTF :: SMPMsgMeta -> ACommand Agent AEConn
|
||||
ACK :: AgentMsgId -> Maybe MsgReceiptInfo -> ACommand Client AEConn
|
||||
RCVD :: MsgMeta -> NonEmpty MsgReceipt -> ACommand Agent AEConn
|
||||
QCONT :: ACommand Agent AEConn
|
||||
SWCH :: ACommand Client AEConn
|
||||
OFF :: ACommand Client AEConn
|
||||
DEL :: ACommand Client AEConn
|
||||
@@ -467,6 +468,7 @@ data ACommandTag (p :: AParty) (e :: AEntity) where
|
||||
MSGNTF_ :: ACommandTag Agent AEConn
|
||||
ACK_ :: ACommandTag Client AEConn
|
||||
RCVD_ :: ACommandTag Agent AEConn
|
||||
QCONT_ :: ACommandTag Agent AEConn
|
||||
SWCH_ :: ACommandTag Client AEConn
|
||||
OFF_ :: ACommandTag Client AEConn
|
||||
DEL_ :: ACommandTag Client AEConn
|
||||
@@ -522,6 +524,7 @@ aCommandTag = \case
|
||||
MSGNTF {} -> MSGNTF_
|
||||
ACK {} -> ACK_
|
||||
RCVD {} -> RCVD_
|
||||
QCONT -> QCONT_
|
||||
SWCH -> SWCH_
|
||||
OFF -> OFF_
|
||||
DEL -> DEL_
|
||||
@@ -996,7 +999,7 @@ agentMessageType = \case
|
||||
HELLO -> AM_HELLO_
|
||||
A_MSG _ -> AM_A_MSG_
|
||||
A_RCVD {} -> AM_A_RCVD_
|
||||
QCONT _ -> AM_QCONT_
|
||||
A_QCONT _ -> AM_QCONT_
|
||||
QADD _ -> AM_QADD_
|
||||
QKEY _ -> AM_QKEY_
|
||||
QUSE _ -> AM_QUSE_
|
||||
@@ -1020,7 +1023,7 @@ data AMsgType
|
||||
= HELLO_
|
||||
| A_MSG_
|
||||
| A_RCVD_
|
||||
| QCONT_
|
||||
| A_QCONT_
|
||||
| QADD_
|
||||
| QKEY_
|
||||
| QUSE_
|
||||
@@ -1033,7 +1036,7 @@ instance Encoding AMsgType where
|
||||
HELLO_ -> "H"
|
||||
A_MSG_ -> "M"
|
||||
A_RCVD_ -> "V"
|
||||
QCONT_ -> "QC"
|
||||
A_QCONT_ -> "QC"
|
||||
QADD_ -> "QA"
|
||||
QKEY_ -> "QK"
|
||||
QUSE_ -> "QU"
|
||||
@@ -1046,7 +1049,7 @@ instance Encoding AMsgType where
|
||||
'V' -> pure A_RCVD_
|
||||
'Q' ->
|
||||
A.anyChar >>= \case
|
||||
'C' -> pure QCONT_
|
||||
'C' -> pure A_QCONT_
|
||||
'A' -> pure QADD_
|
||||
'K' -> pure QKEY_
|
||||
'U' -> pure QUSE_
|
||||
@@ -1066,7 +1069,7 @@ data AMessage
|
||||
| -- | agent envelope for delivery receipt
|
||||
A_RCVD (NonEmpty AMessageReceipt)
|
||||
| -- | the message instructing the client to continue sending messages (after ERR QUOTA)
|
||||
QCONT SndQAddr
|
||||
A_QCONT SndQAddr
|
||||
| -- add queue to connection (sent by recipient), with optional address of the replaced queue
|
||||
QADD (NonEmpty (SMPQueueUri, Maybe SndQAddr))
|
||||
| -- key to secure the added queues and agree e2e encryption key (sent by sender)
|
||||
@@ -1124,7 +1127,7 @@ instance Encoding AMessage where
|
||||
HELLO -> smpEncode HELLO_
|
||||
A_MSG body -> smpEncode (A_MSG_, Tail body)
|
||||
A_RCVD mrs -> smpEncode (A_RCVD_, mrs)
|
||||
QCONT addr -> smpEncode (QCONT_, addr)
|
||||
A_QCONT addr -> smpEncode (A_QCONT_, addr)
|
||||
QADD qs -> smpEncode (QADD_, qs)
|
||||
QKEY qs -> smpEncode (QKEY_, qs)
|
||||
QUSE qs -> smpEncode (QUSE_, qs)
|
||||
@@ -1136,7 +1139,7 @@ instance Encoding AMessage where
|
||||
HELLO_ -> pure HELLO
|
||||
A_MSG_ -> A_MSG . unTail <$> smpP
|
||||
A_RCVD_ -> A_RCVD <$> smpP
|
||||
QCONT_ -> QCONT <$> smpP
|
||||
A_QCONT_ -> A_QCONT <$> smpP
|
||||
QADD_ -> QADD <$> smpP
|
||||
QKEY_ -> QKEY <$> smpP
|
||||
QUSE_ -> QUSE <$> smpP
|
||||
@@ -1668,6 +1671,7 @@ instance StrEncoding ACmdTag where
|
||||
"MSGNTF" -> ct MSGNTF_
|
||||
"ACK" -> t ACK_
|
||||
"RCVD" -> ct RCVD_
|
||||
"QCONT" -> ct QCONT_
|
||||
"SWCH" -> t SWCH_
|
||||
"OFF" -> t OFF_
|
||||
"DEL" -> t DEL_
|
||||
@@ -1725,6 +1729,7 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where
|
||||
MSGNTF_ -> "MSGNTF"
|
||||
ACK_ -> "ACK"
|
||||
RCVD_ -> "RCVD"
|
||||
QCONT_ -> "QCONT"
|
||||
SWCH_ -> "SWCH"
|
||||
OFF_ -> "OFF"
|
||||
DEL_ -> "DEL"
|
||||
@@ -1794,6 +1799,7 @@ commandP binaryP =
|
||||
MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP)
|
||||
MSGNTF_ -> s (MSGNTF <$> strP)
|
||||
RCVD_ -> s (RCVD <$> strP <* A.space <*> strP)
|
||||
QCONT_ -> pure QCONT
|
||||
DEL_RCVQ_ -> s (DEL_RCVQ <$> strP_ <*> strP_ <*> strP)
|
||||
DEL_CONN_ -> pure DEL_CONN
|
||||
DEL_USER_ -> s (DEL_USER <$> strP)
|
||||
@@ -1857,6 +1863,7 @@ serializeCommand = \case
|
||||
MSGNTF smpMsgMeta -> s (MSGNTF_, smpMsgMeta)
|
||||
ACK mId rcptInfo_ -> s (ACK_, mId) <> maybe "" (B.cons ' ' . serializeBinary) rcptInfo_
|
||||
RCVD msgMeta rcpts -> s (RCVD_, msgMeta, rcpts)
|
||||
QCONT -> s QCONT_
|
||||
SWCH -> s SWCH_
|
||||
OFF -> s OFF_
|
||||
DEL -> s DEL_
|
||||
@@ -1897,15 +1904,15 @@ tGetRaw :: Transport c => c -> IO ARawTransmission
|
||||
tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h
|
||||
|
||||
-- | Send SMP agent protocol command (or response) to TCP connection.
|
||||
tPut :: (Transport c, MonadIO m) => c -> ATransmission p -> m ()
|
||||
tPut :: Transport c => c -> ATransmission p -> IO ()
|
||||
tPut h (corrId, connId, APC _ cmd) =
|
||||
liftIO $ tPutRaw h (corrId, connId, serializeCommand cmd)
|
||||
tPutRaw h (corrId, connId, serializeCommand cmd)
|
||||
|
||||
-- | Receive client and agent transmissions from TCP connection.
|
||||
tGet :: forall c m p. (Transport c, MonadIO m) => SAParty p -> c -> m (ATransmissionOrError p)
|
||||
tGet :: forall c p. Transport c => SAParty p -> c -> IO (ATransmissionOrError p)
|
||||
tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
where
|
||||
tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p)
|
||||
tParseLoadBody :: ARawTransmission -> IO (ATransmissionOrError p)
|
||||
tParseLoadBody t@(corrId, entId, command) = do
|
||||
let cmd = parseCommand command >>= fromParty >>= tConnId t
|
||||
fullCmd <- either (return . Left) cmdWithMsgBody cmd
|
||||
@@ -1935,7 +1942,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
| B.null entId -> Left $ CMD NO_CONN
|
||||
| otherwise -> Right cmd
|
||||
|
||||
cmdWithMsgBody :: APartyCmd p -> m (Either AgentErrorType (APartyCmd p))
|
||||
cmdWithMsgBody :: APartyCmd p -> IO (Either AgentErrorType (APartyCmd p))
|
||||
cmdWithMsgBody (APC e cmd) =
|
||||
APC e <$$> case cmd of
|
||||
SEND pqEnc msgFlags body -> SEND pqEnc msgFlags <$$> getBody body
|
||||
@@ -1948,7 +1955,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
INFO pqSup cInfo -> INFO pqSup <$$> getBody cInfo
|
||||
_ -> pure $ Right cmd
|
||||
|
||||
getBody :: ByteString -> m (Either AgentErrorType ByteString)
|
||||
getBody :: ByteString -> IO (Either AgentErrorType ByteString)
|
||||
getBody binary =
|
||||
case B.unpack binary of
|
||||
':' : body -> return . Right $ B.pack body
|
||||
|
||||
@@ -11,6 +11,7 @@ module Simplex.Messaging.Agent.RetryInterval
|
||||
withRetryIntervalCount,
|
||||
withRetryLock2,
|
||||
updateRetryInterval2,
|
||||
nextRetryDelay,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -60,7 +61,7 @@ withRetryIntervalCount ri action = callAction 0 0 $ initialInterval ri
|
||||
loop = do
|
||||
liftIO $ threadDelay' delay
|
||||
let elapsed' = elapsed + delay
|
||||
callAction (n + 1) elapsed' $ nextDelay elapsed' delay ri
|
||||
callAction (n + 1) elapsed' $ nextRetryDelay elapsed' delay ri
|
||||
|
||||
-- This function allows action to toggle between slow and fast retry intervals.
|
||||
withRetryLock2 :: forall m. MonadIO m => RetryInterval2 -> TMVar () -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
|
||||
@@ -76,7 +77,7 @@ withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
|
||||
run (elapsed, delay) ri call = do
|
||||
wait delay
|
||||
let elapsed' = elapsed + delay
|
||||
delay' = nextDelay elapsed' delay ri
|
||||
delay' = nextRetryDelay elapsed' delay ri
|
||||
call (elapsed', delay')
|
||||
wait delay = do
|
||||
waiting <- newTVarIO True
|
||||
@@ -87,8 +88,8 @@ withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
|
||||
takeTMVar lock
|
||||
writeTVar waiting False
|
||||
|
||||
nextDelay :: Int64 -> Int64 -> RetryInterval -> Int64
|
||||
nextDelay elapsed delay RetryInterval {increaseAfter, maxInterval} =
|
||||
nextRetryDelay :: Int64 -> Int64 -> RetryInterval -> Int64
|
||||
nextRetryDelay elapsed delay RetryInterval {increaseAfter, maxInterval} =
|
||||
if elapsed < increaseAfter || delay == maxInterval
|
||||
then delay
|
||||
else min (delay * 3 `div` 2) maxInterval
|
||||
|
||||
@@ -12,13 +12,13 @@ where
|
||||
|
||||
import Control.Logger.Simple (logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (MonadRandom)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Network.Socket (ServiceName)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Client (newAgentClient)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
|
||||
@@ -32,7 +32,7 @@ import UnliftIO.STM
|
||||
-- | Runs an SMP agent as a TCP service using passed configuration.
|
||||
--
|
||||
-- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs
|
||||
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> m ()
|
||||
runSMPAgent :: ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> IO ()
|
||||
runSMPAgent t cfg initServers store =
|
||||
runSMPAgentBlocking t cfg initServers store 0 =<< newEmptyTMVarIO
|
||||
|
||||
@@ -40,44 +40,46 @@ runSMPAgent t cfg initServers store =
|
||||
--
|
||||
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
|
||||
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
|
||||
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> Int -> TMVar Bool -> m ()
|
||||
runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store initClientId started = do
|
||||
liftIO (newSMPAgentEnv cfg store) >>= runReaderT (smpAgent t)
|
||||
runSMPAgentBlocking :: ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> Int -> TMVar Bool -> IO ()
|
||||
runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store initClientId started =
|
||||
case tcpPort of
|
||||
Just port -> newSMPAgentEnv cfg store >>= smpAgent t port
|
||||
Nothing -> E.throwIO $ userError "no agent port"
|
||||
where
|
||||
smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' ()
|
||||
smpAgent _ = do
|
||||
smpAgent :: forall c. Transport c => TProxy c -> ServiceName -> Env -> IO ()
|
||||
smpAgent _ port env = do
|
||||
-- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation
|
||||
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
clientId <- newTVarIO initClientId
|
||||
runTransportServer started tcpPort tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do
|
||||
liftIO . putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion
|
||||
runTransportServer started port tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do
|
||||
putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion
|
||||
cId <- atomically $ stateTVar clientId $ \i -> (i + 1, i + 1)
|
||||
c <- getAgentClient cId initServers
|
||||
c <- atomically $ newAgentClient cId initServers env
|
||||
logConnection c True
|
||||
race_ (connectClient h c) (runAgentClient c)
|
||||
`E.finally` disconnectAgentClient c
|
||||
race_ (connectClient h c) (runAgentClient c `runReaderT` env)
|
||||
`E.finally` (disconnectAgentClient c)
|
||||
|
||||
connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m ()
|
||||
connectClient :: Transport c => c -> AgentClient -> IO ()
|
||||
connectClient h c = race_ (send h c) (receive h c)
|
||||
|
||||
receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
|
||||
receive :: forall c. Transport c => c -> AgentClient -> IO ()
|
||||
receive h c@AgentClient {rcvQ, subQ} = forever $ do
|
||||
(corrId, entId, cmdOrErr) <- tGet SClient h
|
||||
case cmdOrErr of
|
||||
Right cmd -> write rcvQ (corrId, entId, cmd)
|
||||
Left e -> write subQ (corrId, entId, APC SAEConn $ ERR e)
|
||||
where
|
||||
write :: TBQueue (ATransmission p) -> ATransmission p -> m ()
|
||||
write :: TBQueue (ATransmission p) -> ATransmission p -> IO ()
|
||||
write q t = do
|
||||
logClient c "-->" t
|
||||
atomically $ writeTBQueue q t
|
||||
|
||||
send :: (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
|
||||
send :: Transport c => c -> AgentClient -> IO ()
|
||||
send h c@AgentClient {subQ} = forever $ do
|
||||
t <- atomically $ readTBQueue subQ
|
||||
tPut h t
|
||||
logClient c "<--" t
|
||||
|
||||
logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m ()
|
||||
logClient :: AgentClient -> ByteString -> ATransmission a -> IO ()
|
||||
logClient AgentClient {clientId} dir (corrId, connId, APC _ cmd) = do
|
||||
logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd]
|
||||
|
||||
@@ -1214,7 +1214,7 @@ setRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 pqPrivKem =
|
||||
db
|
||||
[sql|
|
||||
UPDATE ratchets
|
||||
SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, x3dh_pub_key_1 = ?, x3dh_pub_key_2 = ?, pq_priv_kem = ?
|
||||
SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, x3dh_pub_key_1 = ?, x3dh_pub_key_2 = ?, pq_priv_kem = ?
|
||||
WHERE conn_id = ?
|
||||
|]
|
||||
(x3dhPrivKey1, x3dhPrivKey2, C.publicKey x3dhPrivKey1, C.publicKey x3dhPrivKey2, pqPrivKem, connId)
|
||||
|
||||
@@ -71,7 +71,8 @@ dbBusyLoop action = loop 500 3000000
|
||||
loop :: Int -> Int -> IO a
|
||||
loop t tLim =
|
||||
action `E.catch` \(e :: SQLError) ->
|
||||
if tLim > t && SQL.sqlError e == SQL.ErrorBusy
|
||||
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)
|
||||
|
||||
+172
-36
@@ -54,6 +54,9 @@ module Simplex.Messaging.Client
|
||||
suspendSMPQueue,
|
||||
deleteSMPQueue,
|
||||
deleteSMPQueues,
|
||||
createSMPProxySession,
|
||||
proxySMPMessage,
|
||||
forwardSMPMessage,
|
||||
sendProtocolCommand,
|
||||
|
||||
-- * Supporting types and client configuration
|
||||
@@ -69,6 +72,7 @@ module Simplex.Messaging.Client
|
||||
chooseTransportHost,
|
||||
proxyUsername,
|
||||
temporaryClientError,
|
||||
smpProxyError,
|
||||
ServerTransmission,
|
||||
ClientCommand,
|
||||
|
||||
@@ -84,8 +88,8 @@ import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
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
|
||||
@@ -97,10 +101,13 @@ import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import Data.Time.Clock (UTCTime (..), 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 +117,7 @@ import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), runTransportClient)
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (bshow, raceAny_, threadDelay')
|
||||
import Simplex.Messaging.Util (bshow, liftEitherWith, raceAny_, threadDelay')
|
||||
import Simplex.Messaging.Version
|
||||
import System.Timeout (timeout)
|
||||
|
||||
@@ -119,7 +126,7 @@ import System.Timeout (timeout)
|
||||
-- Use 'getSMPClient' to connect to an SMP server and create a client handle.
|
||||
data ProtocolClient v err msg = ProtocolClient
|
||||
{ action :: Maybe (Async ()),
|
||||
thParams :: THandleParams v,
|
||||
thParams :: THandleParams v 'TClient,
|
||||
sessionTs :: UTCTime,
|
||||
client_ :: PClient v err msg
|
||||
}
|
||||
@@ -129,7 +136,6 @@ data PClient v err msg = PClient
|
||||
transportSession :: TransportSession msg,
|
||||
transportHost :: TransportHost,
|
||||
tcpTimeout :: Int,
|
||||
batchDelay :: Maybe Int,
|
||||
pingErrorCount :: TVar Int,
|
||||
clientCorrId :: TVar ChaChaDRG,
|
||||
sentCommands :: TMap CorrId (Request err msg),
|
||||
@@ -138,11 +144,12 @@ data PClient v err msg = PClient
|
||||
msgQ :: Maybe (TBQueue (ServerTransmission v msg))
|
||||
}
|
||||
|
||||
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe THandleAuth -> STM (ProtocolClient SMPVersion err msg)
|
||||
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient
|
||||
smpClientStub g sessionId thVersion thAuth = do
|
||||
connected <- newTVar False
|
||||
clientCorrId <- C.newRandomDRG g
|
||||
sentCommands <- TM.empty
|
||||
pingErrorCount <- newTVar 0
|
||||
sndQ <- newTBQueue 100
|
||||
rcvQ <- newTBQueue 100
|
||||
return
|
||||
@@ -157,15 +164,14 @@ smpClientStub g sessionId thVersion thAuth = do
|
||||
implySessId = thVersion >= authCmdsSMPVersion,
|
||||
batch = True
|
||||
},
|
||||
sessionTs = undefined,
|
||||
sessionTs = UTCTime (read "2024-03-31") 0,
|
||||
client_ =
|
||||
PClient
|
||||
{ connected,
|
||||
transportSession = undefined,
|
||||
transportHost = undefined,
|
||||
tcpTimeout = undefined,
|
||||
batchDelay = Nothing,
|
||||
pingErrorCount = undefined,
|
||||
transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing),
|
||||
transportHost = "localhost",
|
||||
tcpTimeout = 15_000_000,
|
||||
pingErrorCount,
|
||||
clientCorrId,
|
||||
sentCommands,
|
||||
sndQ,
|
||||
@@ -206,7 +212,7 @@ data NetworkConfig = NetworkConfig
|
||||
-- | timeout of protocol commands (microseconds)
|
||||
tcpTimeout :: Int,
|
||||
-- | additional timeout per kilobyte (1024 bytes) to be sent
|
||||
tcpTimeoutPerKb :: Int,
|
||||
tcpTimeoutPerKb :: Int64,
|
||||
-- | TCP keep-alive options, Nothing to skip enabling keep-alive
|
||||
tcpKeepAlive :: Maybe KeepAliveOpts,
|
||||
-- | period for SMP ping commands (microseconds, 0 to disable)
|
||||
@@ -229,7 +235,7 @@ defaultNetworkConfig =
|
||||
sessionMode = TSMUser,
|
||||
tcpConnectTimeout = 20_000_000,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpTimeoutPerKb = 45_000, -- 45ms, should be less than 130ms to avoid Int overflow on 32 bit systems
|
||||
tcpTimeoutPerKb = 5_000,
|
||||
tcpKeepAlive = Just defaultKeepAliveOpts,
|
||||
smpPingInterval = 600_000_000, -- 10min
|
||||
smpPingCount = 3,
|
||||
@@ -238,7 +244,8 @@ defaultNetworkConfig =
|
||||
|
||||
transportClientConfig :: NetworkConfig -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, tcpKeepAlive, logTLSErrors} =
|
||||
TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing}
|
||||
TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
|
||||
{-# INLINE transportClientConfig #-}
|
||||
|
||||
-- | protocol client configuration.
|
||||
data ProtocolClientConfig v = ProtocolClientConfig
|
||||
@@ -250,8 +257,8 @@ data ProtocolClientConfig v = ProtocolClientConfig
|
||||
networkConfig :: NetworkConfig,
|
||||
-- | client-server protocol version range
|
||||
serverVRange :: VersionRange v,
|
||||
-- | delay between sending batches of commands (microseconds)
|
||||
batchDelay :: Maybe Int
|
||||
-- | agree shared session secret (used in SMP proxy)
|
||||
agreeSecret :: Bool
|
||||
}
|
||||
|
||||
-- | Default protocol client configuration.
|
||||
@@ -262,11 +269,13 @@ defaultClientConfig serverVRange =
|
||||
defaultTransport = ("443", transport @TLS),
|
||||
networkConfig = defaultNetworkConfig,
|
||||
serverVRange,
|
||||
batchDelay = Nothing
|
||||
agreeSecret = False
|
||||
}
|
||||
{-# INLINE defaultClientConfig #-}
|
||||
|
||||
defaultSMPClientConfig :: ProtocolClientConfig SMPVersion
|
||||
defaultSMPClientConfig = defaultClientConfig supportedClientSMPRelayVRange
|
||||
{-# INLINE defaultSMPClientConfig #-}
|
||||
|
||||
data Request err msg = Request
|
||||
{ entityId :: EntityId,
|
||||
@@ -296,12 +305,15 @@ protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient v err ms
|
||||
protocolClientServer = B.unpack . strEncode . snd3 . transportSession . client_
|
||||
where
|
||||
snd3 (_, s, _) = s
|
||||
{-# INLINE protocolClientServer #-}
|
||||
|
||||
transportHost' :: ProtocolClient v err msg -> TransportHost
|
||||
transportHost' = transportHost . client_
|
||||
{-# INLINE transportHost' #-}
|
||||
|
||||
transportSession' :: ProtocolClient v err msg -> TransportSession msg
|
||||
transportSession' = transportSession . client_
|
||||
{-# INLINE transportSession' #-}
|
||||
|
||||
type UserId = Int64
|
||||
|
||||
@@ -314,7 +326,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 g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, serverVRange, batchDelay} msgQ disconnected = do
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, serverVRange, agreeSecret} msgQ disconnected = do
|
||||
case chooseTransportHost networkConfig (host srv) of
|
||||
Right useHost ->
|
||||
(atomically (mkProtocolClient useHost) >>= runClient useTransport useHost)
|
||||
@@ -336,7 +348,6 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
transportSession,
|
||||
transportHost,
|
||||
tcpTimeout,
|
||||
batchDelay,
|
||||
pingErrorCount,
|
||||
clientCorrId,
|
||||
sentCommands,
|
||||
@@ -368,7 +379,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
|
||||
client :: forall c. Transport c => TProxy c -> PClient v err msg -> TMVar (Either (ProtocolClientError err) (ProtocolClient v err msg)) -> c -> IO ()
|
||||
client _ c cVar h = do
|
||||
ks <- atomically $ C.generateKeyPair g
|
||||
ks <- if agreeSecret then Just <$> atomically (C.generateKeyPair g) else pure Nothing
|
||||
runExceptT (protocolClientHandshake @v @err @msg h ks (keyHash srv) serverVRange) >>= \case
|
||||
Left e -> atomically . putTMVar cVar . Left $ PCETransportError e
|
||||
Right th@THandle {params} -> do
|
||||
@@ -380,10 +391,10 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0])
|
||||
`finally` disconnected c'
|
||||
|
||||
send :: Transport c => ProtocolClient v err msg -> THandle v c -> IO ()
|
||||
send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
|
||||
send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= tPutLog h
|
||||
|
||||
receive :: Transport c => ProtocolClient v err msg -> THandle v c -> IO ()
|
||||
receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
|
||||
receive ProtocolClient {client_ = PClient {rcvQ}} h = forever $ tGet h >>= atomically . writeTBQueue rcvQ
|
||||
|
||||
ping :: ProtocolClient v err msg -> IO ()
|
||||
@@ -426,10 +437,12 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
|
||||
proxyUsername :: TransportSession msg -> ByteString
|
||||
proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_
|
||||
{-# INLINE proxyUsername #-}
|
||||
|
||||
-- | Disconnects client from the server and terminates client threads.
|
||||
closeProtocolClient :: ProtocolClient v err msg -> IO ()
|
||||
closeProtocolClient = mapM_ uninterruptibleCancel . action
|
||||
{-# INLINE closeProtocolClient #-}
|
||||
|
||||
-- | SMP client error type.
|
||||
data ProtocolClientError err
|
||||
@@ -469,6 +482,19 @@ temporaryClientError = \case
|
||||
PCEResponseTimeout -> True
|
||||
PCEIOError _ -> True
|
||||
_ -> False
|
||||
{-# INLINE temporaryClientError #-}
|
||||
|
||||
smpProxyError :: SMPClientError -> ErrorType
|
||||
smpProxyError = \case
|
||||
PCEProtocolError et -> PROXY (PROTOCOL et)
|
||||
PCEResponseError et -> PROXY (RESPONSE et)
|
||||
PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs)
|
||||
PCEResponseTimeout -> PROXY TIMEOUT
|
||||
PCENetworkError -> PROXY NETWORK
|
||||
PCEIncompatibleHost -> PROXY BAD_HOST
|
||||
PCETransportError t -> PROXY (TRANSPORT t)
|
||||
PCECryptoError _ -> INTERNAL
|
||||
PCEIOError _ -> INTERNAL
|
||||
|
||||
-- | Create a new SMP queue.
|
||||
--
|
||||
@@ -536,16 +562,19 @@ getSMPMessage c rpKey rId =
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue-notifications
|
||||
subscribeSMPQueueNotifications :: SMPClient -> NtfPrivateAuthKey -> NotifierId -> ExceptT SMPClientError IO ()
|
||||
subscribeSMPQueueNotifications = okSMPCommand NSUB
|
||||
{-# INLINE subscribeSMPQueueNotifications #-}
|
||||
|
||||
-- | Subscribe to multiple SMP queues notifications batching commands if supported.
|
||||
subscribeSMPQueuesNtfs :: SMPClient -> NonEmpty (NtfPrivateAuthKey, NotifierId) -> IO (NonEmpty (Either SMPClientError ()))
|
||||
subscribeSMPQueuesNtfs = okSMPCommands NSUB
|
||||
{-# INLINE subscribeSMPQueuesNtfs #-}
|
||||
|
||||
-- | Secure the SMP queue by adding a sender public key.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#secure-queue-command
|
||||
secureSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> SndPublicAuthKey -> ExceptT SMPClientError IO ()
|
||||
secureSMPQueue c rpKey rId senderKey = okSMPCommand (KEY senderKey) c rpKey rId
|
||||
{-# INLINE secureSMPQueue #-}
|
||||
|
||||
-- | Enable notifications for the queue for push notifications server.
|
||||
--
|
||||
@@ -571,10 +600,12 @@ enableSMPQueuesNtfs c qs = L.map process <$> sendProtocolCommands c cs
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#disable-notifications-command
|
||||
disableSMPQueueNotifications :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
|
||||
disableSMPQueueNotifications = okSMPCommand NDEL
|
||||
{-# INLINE disableSMPQueueNotifications #-}
|
||||
|
||||
-- | Disable notifications for multiple queues for push notifications server.
|
||||
disableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
|
||||
disableSMPQueuesNtfs = okSMPCommands NDEL
|
||||
{-# INLINE disableSMPQueuesNtfs #-}
|
||||
|
||||
-- | Send SMP message.
|
||||
--
|
||||
@@ -601,16 +632,115 @@ ackSMPMessage c rpKey rId msgId =
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#suspend-queue
|
||||
suspendSMPQueue :: SMPClient -> RcvPrivateAuthKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
suspendSMPQueue = okSMPCommand OFF
|
||||
{-# INLINE suspendSMPQueue #-}
|
||||
|
||||
-- | Irreversibly delete SMP queue and all messages in it.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#delete-queue
|
||||
deleteSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
|
||||
deleteSMPQueue = okSMPCommand DEL
|
||||
{-# INLINE deleteSMPQueue #-}
|
||||
|
||||
-- | Delete multiple SMP queues batching commands if supported.
|
||||
deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
|
||||
deleteSMPQueues = okSMPCommands DEL
|
||||
{-# INLINE deleteSMPQueues #-}
|
||||
|
||||
-- TODO picture
|
||||
|
||||
-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender
|
||||
-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg
|
||||
createSMPProxySession :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO (SessionId, VersionSMP, C.PublicKeyX25519)
|
||||
createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth =
|
||||
sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case
|
||||
-- XXX: rfc says sessionId should be in the entityId of response
|
||||
PKEY sId vr (chain, key) -> do
|
||||
case supportedClientSMPRelayVRange `compatibleVersion` vr of
|
||||
Nothing -> throwE PCEIncompatibleHost -- TODO different error
|
||||
Just (Compatible v) -> liftEitherWith x509Error $ (sId,v,) <$> validateRelay chain key
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
where
|
||||
x509Error :: String -> SMPClientError
|
||||
x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error
|
||||
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
|
||||
|
||||
-- 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
|
||||
proxySMPMessage ::
|
||||
SMPClient ->
|
||||
-- proxy session from PKEY
|
||||
SessionId ->
|
||||
VersionSMP ->
|
||||
C.PublicKeyX25519 ->
|
||||
-- message to deliver
|
||||
Maybe SndPrivateAuthKey ->
|
||||
SenderId ->
|
||||
MsgFlags ->
|
||||
MsgBody ->
|
||||
ExceptT SMPClientError IO ()
|
||||
-- TODO use version
|
||||
proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} sessionId _v serverKey spKey sId flags msg = do
|
||||
-- prepare params
|
||||
let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams
|
||||
serverThParams = 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 -- some other error. Internal?
|
||||
TBError e _ : _ -> throwE $ PCETransportError e -- large message error?
|
||||
TBTransmission s _ : _ -> pure s
|
||||
TBTransmissions s _ _ : _ -> pure s
|
||||
et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength
|
||||
sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case
|
||||
-- TODO support PKEY + resend?
|
||||
PRES (EncResponse er) -> do
|
||||
t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er
|
||||
case tParse proxyThParams t' of
|
||||
t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of
|
||||
(_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify
|
||||
Left e -> throwE $ PCEResponseError e
|
||||
Right OK -> pure ()
|
||||
Right (ERR e) -> throwE $ PCEProtocolError e
|
||||
Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy
|
||||
_ -> throwE $ PCETransportError TEBadBlock
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy
|
||||
|
||||
-- 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 -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse
|
||||
forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do
|
||||
-- prepare params
|
||||
sessSecret <- case thAuth thParams of
|
||||
Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key?
|
||||
Just THAuthClient {sessSecret} -> maybe (throwError $ PCEProtocolError INTERNAL) pure sessSecret
|
||||
nonce <- liftIO . atomically $ C.randomCbNonce g
|
||||
-- wrap
|
||||
let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission}
|
||||
eft <- liftEitherWith PCECryptoError $ EncFwdTransmission <$> C.cbEncrypt sessSecret nonce (smpEncode fwdT) paddedForwardedMsgLength
|
||||
-- send
|
||||
sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case
|
||||
RRES (EncFwdResponse efr) -> do
|
||||
-- unwrap
|
||||
r' <- liftEitherWith PCECryptoError $ C.cbDecrypt sessSecret (C.reverseNonce nonce) efr
|
||||
FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r'
|
||||
pure fwdResponse
|
||||
r -> throwE . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO ()
|
||||
okSMPCommand cmd c pKey qId =
|
||||
@@ -631,6 +761,7 @@ okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs
|
||||
-- | Send SMP command
|
||||
sendSMPCommand :: PartyI p => SMPClient -> Maybe C.APrivateAuthKey -> QueueId -> Command p -> ExceptT SMPClientError IO BrokerMsg
|
||||
sendSMPCommand c pKey qId cmd = sendProtocolCommand c pKey qId (Cmd sParty cmd)
|
||||
{-# INLINE sendSMPCommand #-}
|
||||
|
||||
type PCTransmission err msg = (Either TransportError SentRawTransmission, Request err msg)
|
||||
|
||||
@@ -674,8 +805,11 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do
|
||||
|
||||
-- | 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 c = sendProtocolCommand_ c Nothing
|
||||
|
||||
sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
|
||||
sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ 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)
|
||||
@@ -694,33 +828,35 @@ getResponse :: ProtocolClient v err msg -> Request err msg -> IO (Response err m
|
||||
getResponse ProtocolClient {client_ = PClient {tcpTimeout, pingErrorCount}} Request {entityId, responseVar} = do
|
||||
response <-
|
||||
timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case
|
||||
-- BTW: another registerDelay candidate. Also, crashes caller with BlockedIndef.
|
||||
Just r -> atomically (writeTVar pingErrorCount 0) $> r
|
||||
Nothing -> pure $ 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 :: ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg)
|
||||
mkTransmission c = mkTransmission_ c Nothing
|
||||
|
||||
mkTransmission_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> ClientCommand msg -> IO (PCTransmission err msg)
|
||||
mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (pKey_, entId, cmd) = do
|
||||
nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entId, cmd)
|
||||
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 entId <$> newEmptyTMVar
|
||||
TM.insert corrId r sentCommands
|
||||
pure r
|
||||
|
||||
authTransmission :: Maybe THandleAuth -> 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 THandleAuth {peerPubKey} -> Right $ TAAuthenticator $ C.cbAuthenticate peerPubKey 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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -18,6 +19,7 @@ import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Trans.Except
|
||||
import Control.Monad.Trans.Reader
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -38,6 +40,7 @@ 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.Session
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
@@ -48,7 +51,7 @@ import UnliftIO.Exception (Exception)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
type SMPClientVar = TMVar (Either SMPClientError SMPClient)
|
||||
type SMPClientVar = SessionVar (Either SMPClientError SMPClient)
|
||||
|
||||
data SMPClientAgentEvent
|
||||
= CAConnected SMPServer
|
||||
@@ -95,10 +98,12 @@ data SMPClientAgent = SMPClientAgent
|
||||
agentQ :: TBQueue SMPClientAgentEvent,
|
||||
randomDrg :: TVar ChaChaDRG,
|
||||
smpClients :: TMap SMPServer SMPClientVar,
|
||||
smpSessions :: TMap SessionId SMPClient,
|
||||
srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
|
||||
pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
|
||||
reconnections :: TVar [Async ()],
|
||||
asyncClients :: TVar [Async ()]
|
||||
asyncClients :: TVar [Async ()],
|
||||
workerSeq :: TVar Int
|
||||
}
|
||||
|
||||
newtype InternalException e = InternalException {unInternalException :: e}
|
||||
@@ -106,83 +111,108 @@ newtype InternalException e = InternalException {unInternalException :: e}
|
||||
|
||||
instance Exception e => Exception (InternalException e)
|
||||
|
||||
instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
|
||||
withRunInIO :: ((forall a. ExceptT e m a -> IO a) -> IO b) -> ExceptT e m b
|
||||
withRunInIO exceptToIO =
|
||||
instance Exception e => MonadUnliftIO (ExceptT e IO) where
|
||||
{-# INLINE withRunInIO #-}
|
||||
withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b
|
||||
withRunInIO inner =
|
||||
ExceptT . fmap (first unInternalException) . E.try $
|
||||
withRunInIO $ \run ->
|
||||
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
|
||||
|
||||
-- as MonadUnliftIO instance for IO is `withRunInIO inner = inner id`,
|
||||
-- the last two lines could be replaced with:
|
||||
-- inner $ either (E.throwIO . InternalException) pure <=< runExceptT
|
||||
|
||||
instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where
|
||||
{-# INLINE withRunInIO #-}
|
||||
withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b
|
||||
withRunInIO inner =
|
||||
withExceptT unInternalException . ExceptT . E.try $
|
||||
withRunInIO $ \run ->
|
||||
exceptToIO $ run . (either (E.throwIO . InternalException) return <=< runExceptT)
|
||||
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
|
||||
|
||||
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent
|
||||
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do
|
||||
msgQ <- newTBQueue msgQSize
|
||||
agentQ <- newTBQueue agentQSize
|
||||
smpClients <- TM.empty
|
||||
smpSessions <- TM.empty
|
||||
srvSubs <- TM.empty
|
||||
pendingSrvSubs <- TM.empty
|
||||
reconnections <- newTVar []
|
||||
asyncClients <- newTVar []
|
||||
pure SMPClientAgent {agentCfg, msgQ, agentQ, randomDrg, smpClients, srvSubs, pendingSrvSubs, reconnections, asyncClients}
|
||||
workerSeq <- newTVar 0
|
||||
pure
|
||||
SMPClientAgent
|
||||
{ agentCfg,
|
||||
msgQ,
|
||||
agentQ,
|
||||
randomDrg,
|
||||
smpClients,
|
||||
smpSessions,
|
||||
srvSubs,
|
||||
pendingSrvSubs,
|
||||
reconnections,
|
||||
asyncClients,
|
||||
workerSeq
|
||||
}
|
||||
|
||||
getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient
|
||||
getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg} srv =
|
||||
getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, workerSeq} srv =
|
||||
atomically getClientVar >>= either newSMPClient waitForSMPClient
|
||||
where
|
||||
getClientVar :: STM (Either SMPClientVar SMPClientVar)
|
||||
getClientVar = maybe (Left <$> newClientVar) (pure . Right) =<< TM.lookup srv smpClients
|
||||
|
||||
newClientVar :: STM SMPClientVar
|
||||
newClientVar = do
|
||||
smpVar <- newEmptyTMVar
|
||||
TM.insert srv smpVar smpClients
|
||||
pure smpVar
|
||||
getClientVar = getSessVar workerSeq srv smpClients
|
||||
|
||||
waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
|
||||
waitForSMPClient smpVar = do
|
||||
waitForSMPClient v = do
|
||||
let ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg
|
||||
smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar smpVar)
|
||||
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
|
||||
|
||||
newSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
|
||||
newSMPClient smpVar = tryConnectClient pure tryConnectAsync
|
||||
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 >>= \r -> case r of
|
||||
tryE (connectClient v) >>= \r -> case r of
|
||||
Right smp -> do
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
|
||||
atomically $ putTMVar smpVar r
|
||||
atomically $ do
|
||||
putTMVar (sessionVar v) r
|
||||
TM.insert (sessionId $ thParams smp) smp smpSessions
|
||||
successAction smp
|
||||
Left e -> do
|
||||
if e == PCENetworkError || e == PCEResponseTimeout
|
||||
then retryAction
|
||||
else atomically $ do
|
||||
putTMVar smpVar (Left e)
|
||||
TM.delete srv smpClients
|
||||
putTMVar (sessionVar v) (Left e)
|
||||
removeSessVar v srv smpClients
|
||||
throwE e
|
||||
tryConnectAsync :: ExceptT SMPClientError IO ()
|
||||
tryConnectAsync :: IO ()
|
||||
tryConnectAsync = do
|
||||
a <- async connectAsync
|
||||
a <- async $ void $ runExceptT connectAsync
|
||||
atomically $ modifyTVar' (asyncClients ca) (a :)
|
||||
connectAsync :: ExceptT SMPClientError IO ()
|
||||
connectAsync =
|
||||
withRetryInterval (reconnectInterval agentCfg) $ \_ loop ->
|
||||
void $ tryConnectClient (const reconnectClient) loop
|
||||
|
||||
connectClient :: ExceptT SMPClientError IO SMPClient
|
||||
connectClient = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected
|
||||
connectClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
|
||||
connectClient v = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) (clientDisconnected v)
|
||||
|
||||
clientDisconnected :: SMPClient -> IO ()
|
||||
clientDisconnected _ = do
|
||||
removeClientAndSubs >>= (`forM_` serverDown)
|
||||
clientDisconnected :: SMPClientVar -> SMPClient -> IO ()
|
||||
clientDisconnected v smp = do
|
||||
removeClientAndSubs v smp >>= (`forM_` serverDown)
|
||||
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
||||
|
||||
removeClientAndSubs :: IO (Maybe (Map SMPSub C.APrivateAuthKey))
|
||||
removeClientAndSubs = atomically $ do
|
||||
TM.delete srv smpClients
|
||||
removeClientAndSubs :: SMPClientVar -> SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey))
|
||||
removeClientAndSubs v smp = atomically $ do
|
||||
removeSessVar v srv smpClients
|
||||
TM.delete (sessionId $ thParams smp) smpSessions
|
||||
TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs
|
||||
where
|
||||
updateSubs sVar = do
|
||||
@@ -193,17 +223,17 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg} sr
|
||||
addPendingSubs sVar ss = do
|
||||
let ps = pendingSrvSubs ca
|
||||
TM.lookup srv ps >>= \case
|
||||
Just v -> TM.union ss v
|
||||
Just ss' -> TM.union ss ss'
|
||||
_ -> TM.insert srv sVar ps
|
||||
|
||||
serverDown :: Map SMPSub C.APrivateAuthKey -> IO ()
|
||||
serverDown ss = unless (M.null ss) $ do
|
||||
notify . CADisconnected srv $ M.keysSet ss
|
||||
void $ runExceptT reconnectServer
|
||||
reconnectServer
|
||||
|
||||
reconnectServer :: ExceptT SMPClientError IO ()
|
||||
reconnectServer :: IO ()
|
||||
reconnectServer = do
|
||||
a <- async tryReconnectClient
|
||||
a <- async $ void $ runExceptT tryReconnectClient
|
||||
atomically $ modifyTVar' (reconnections ca) (a :)
|
||||
|
||||
tryReconnectClient :: ExceptT SMPClientError IO ()
|
||||
@@ -247,17 +277,20 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg} sr
|
||||
notify :: SMPClientAgentEvent -> IO ()
|
||||
notify evt = atomically $ writeTBQueue (agentQ ca) evt
|
||||
|
||||
closeSMPClientAgent :: MonadUnliftIO m => SMPClientAgent -> m ()
|
||||
closeSMPClientAgent c = liftIO $ do
|
||||
lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe SMPClient)
|
||||
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions
|
||||
|
||||
closeSMPClientAgent :: SMPClientAgent -> IO ()
|
||||
closeSMPClientAgent c = do
|
||||
closeSMPServerClients c
|
||||
cancelActions $ reconnections c
|
||||
cancelActions $ asyncClients c
|
||||
|
||||
closeSMPServerClients :: SMPClientAgent -> IO ()
|
||||
closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ (forkIO . closeClient)
|
||||
closeSMPServerClients c = atomically (smpClients c `swapTVar` M.empty) >>= mapM_ (forkIO . closeClient)
|
||||
where
|
||||
closeClient smpVar =
|
||||
atomically (readTMVar smpVar) >>= \case
|
||||
closeClient v =
|
||||
atomically (readTMVar $ sessionVar v) >>= \case
|
||||
Right smp -> closeProtocolClient smp `catchAll_` pure ()
|
||||
_ -> pure ()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
module Simplex.Messaging.Compression where
|
||||
|
||||
import qualified Codec.Compression.Zstd as Z1
|
||||
import qualified Codec.Compression.Zstd.FFI as Z
|
||||
import Control.Monad (forM)
|
||||
import Control.Monad.Except
|
||||
@@ -28,6 +29,9 @@ data Compressed
|
||||
maxLengthPassthrough :: Int
|
||||
maxLengthPassthrough = 180 -- Sampled from real client data. Messages with length > 180 rapidly gain compression ratio.
|
||||
|
||||
compressionLevel :: Num a => a
|
||||
compressionLevel = 3
|
||||
|
||||
instance Encoding Compressed where
|
||||
smpEncode = \case
|
||||
Passthrough bytes -> "0" <> smpEncode bytes
|
||||
@@ -38,6 +42,12 @@ instance Encoding Compressed where
|
||||
'1' -> Compressed <$> smpP
|
||||
x -> fail $ "unknown Compressed tag: " <> show x
|
||||
|
||||
-- | Compress as single chunk using stack-allocated context.
|
||||
compress1 :: ByteString -> Compressed
|
||||
compress1 bs
|
||||
| B.length bs <= maxLengthPassthrough = Passthrough bs
|
||||
| otherwise = Compressed . Large $ Z1.compress compressionLevel bs
|
||||
|
||||
type CompressCtx = (Ptr Z.CCtx, Ptr CChar, CSize)
|
||||
|
||||
withCompressCtx :: CSize -> (CompressCtx -> IO a) -> IO a
|
||||
@@ -56,7 +66,7 @@ compress_ (cctx, scratchPtr, scratchSize) bs
|
||||
| otherwise =
|
||||
B.unsafeUseAsCStringLen bs $ \(sourcePtr, sourceSize) -> runExceptT $ do
|
||||
-- should not fail, unless input buffer is too short
|
||||
dstSize <- ExceptT $ Z.checkError $ Z.compressCCtx cctx scratchPtr scratchSize sourcePtr (fromIntegral sourceSize) 3
|
||||
dstSize <- ExceptT $ Z.checkError $ Z.compressCCtx cctx scratchPtr scratchSize sourcePtr (fromIntegral sourceSize) compressionLevel
|
||||
liftIO $ Compressed . Large <$> B.packCStringLen (scratchPtr, fromIntegral dstSize)
|
||||
|
||||
type DecompressCtx = (Ptr Z.DCtx, Ptr CChar, CSize)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,7 +76,7 @@ withFile :: CryptoFile -> IOMode -> (CryptoFileHandle -> ExceptT FTCryptoError I
|
||||
withFile (CryptoFile path cfArgs) mode action = do
|
||||
sb <- forM cfArgs $ \(CFArgs key nonce) ->
|
||||
liftEitherWith FTCECryptoError (LC.sbInit key nonce) >>= newTVarIO
|
||||
IO.withFile path mode $ \h -> action $ CFHandle h sb
|
||||
ExceptT . IO.withFile path mode $ \h -> runExceptT $ action $ CFHandle h sb
|
||||
|
||||
hPut :: CryptoFileHandle -> LazyByteString -> IO ()
|
||||
hPut (CFHandle h sb_) s = LB.hPut h =<< maybe (pure s) encrypt sb_
|
||||
|
||||
@@ -75,6 +75,8 @@ 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
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Protocol where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Kind
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
@@ -150,7 +152,7 @@ instance Encoding ANewNtfEntity where
|
||||
instance Protocol NTFVersion ErrorType NtfResponse where
|
||||
type ProtoCommand NtfResponse = NtfCmd
|
||||
type ProtoType NtfResponse = 'PNTF
|
||||
protocolClientHandshake = ntfClientHandshake
|
||||
protocolClientHandshake c _ks = ntfClientHandshake c
|
||||
protocolPing = NtfCmd SSubscription PING
|
||||
protocolError = \case
|
||||
NRErr e -> Just e
|
||||
@@ -406,10 +408,12 @@ instance Encoding DeviceToken where
|
||||
|
||||
instance StrEncoding DeviceToken where
|
||||
strEncode (DeviceToken p t) = strEncode p <> " " <> t
|
||||
strP = DeviceToken <$> strP <* A.space <*> hexStringP
|
||||
strP = nullToken <|> hexToken
|
||||
where
|
||||
nullToken = "apns_null test_ntf_token" $> DeviceToken PPApnsNull "test_ntf_token"
|
||||
hexToken = DeviceToken <$> strP <* A.space <*> hexStringP
|
||||
hexStringP =
|
||||
A.takeWhile (\c -> A.isDigit c || (c >= 'a' && c <= 'f')) >>= \s ->
|
||||
A.takeWhile (`B.elem` "0123456789abcdef") >>= \s ->
|
||||
if even (B.length s) then pure s else fail "odd number of hex characters"
|
||||
|
||||
instance ToJSON DeviceToken where
|
||||
|
||||
@@ -23,7 +23,6 @@ import Data.Int (Int64)
|
||||
import Data.List (intercalate, sort)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (catMaybes)
|
||||
import qualified Data.Text as T
|
||||
@@ -48,7 +47,7 @@ import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server
|
||||
import Simplex.Messaging.Server.Stats
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..))
|
||||
import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Util
|
||||
import System.Exit (exitFailure)
|
||||
@@ -75,15 +74,15 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
|
||||
restoreServerStats
|
||||
s <- asks subscriber
|
||||
ps <- asks pushServer
|
||||
subs <- readTVarIO =<< asks (subscriptions . store)
|
||||
void . forkIO $ resubscribe s subs
|
||||
resubscribe s
|
||||
raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports <> serverStatsThread_ cfg) `finally` stopServer
|
||||
where
|
||||
runServer :: (ServiceName, ATransport) -> M ()
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
serverParams <- asks tlsServerParams
|
||||
serverSignKey <- either fail pure . fromTLSCredentials $ tlsServerCredentials serverParams
|
||||
runTransportServer started tcpPort serverParams tCfg (runClient serverSignKey t)
|
||||
env <- ask
|
||||
liftIO $ runTransportServer started tcpPort serverParams tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
|
||||
fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey
|
||||
|
||||
runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M ()
|
||||
@@ -109,7 +108,7 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
|
||||
logServerStats :: Int64 -> Int64 -> FilePath -> M ()
|
||||
logServerStats startAt logInterval statsFilePath = do
|
||||
initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime
|
||||
liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath
|
||||
logInfo $ "server stats log enabled: " <> T.pack statsFilePath
|
||||
liftIO $ threadDelay' $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0)
|
||||
NtfServerStats {fromTime, tknCreated, tknVerified, tknDeleted, subCreated, subDeleted, ntfReceived, ntfDelivered, activeTokens, activeSubs} <- asks serverStats
|
||||
let interval = 1000000 * logInterval
|
||||
@@ -147,11 +146,13 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
|
||||
]
|
||||
liftIO $ threadDelay' interval
|
||||
|
||||
resubscribe :: NtfSubscriber -> Map NtfSubscriptionId NtfSubData -> M ()
|
||||
resubscribe NtfSubscriber {newSubQ} subs = do
|
||||
subs' <- atomically $ filterM (fmap ntfShouldSubscribe . readTVar . subStatus) $ M.elems subs
|
||||
resubscribe :: NtfSubscriber -> M ()
|
||||
resubscribe NtfSubscriber {newSubQ} = do
|
||||
logInfo "Preparing SMP resubscriptions..."
|
||||
subs <- readTVarIO =<< asks (subscriptions . store)
|
||||
subs' <- filterM (fmap ntfShouldSubscribe . readTVarIO . subStatus) $ M.elems subs
|
||||
atomically . writeTBQueue newSubQ $ map NtfSub subs'
|
||||
liftIO $ logInfo $ "SMP resubscriptions queued (" <> tshow (length subs') <> " subscriptions)"
|
||||
logInfo $ "SMP resubscriptions queued (" <> tshow (length subs') <> " subscriptions)"
|
||||
|
||||
ntfSubscriber :: NtfSubscriber -> M ()
|
||||
ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAgent {msgQ, agentQ}} = do
|
||||
@@ -338,7 +339,7 @@ updateTknStatus NtfTknData {ntfTknId, tknStatus} status = do
|
||||
old <- atomically $ stateTVar tknStatus (,status)
|
||||
when (old /= status) $ withNtfLog $ \sl -> logTokenStatus sl ntfTknId status
|
||||
|
||||
runNtfClientTransport :: Transport c => THandleNTF c -> M ()
|
||||
runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M ()
|
||||
runNtfClientTransport th@THandle {params} = do
|
||||
qSize <- asks $ clientQSize . config
|
||||
ts <- liftIO getSystemTime
|
||||
@@ -355,7 +356,7 @@ runNtfClientTransport th@THandle {params} = do
|
||||
clientDisconnected :: NtfServerClient -> IO ()
|
||||
clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connected False
|
||||
|
||||
receive :: Transport c => THandleNTF c -> NtfServerClient -> M ()
|
||||
receive :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> M ()
|
||||
receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do
|
||||
ts <- liftIO $ tGet th
|
||||
forM_ ts $ \t@(_, _, (corrId, entId, cmdOrError)) -> do
|
||||
@@ -370,7 +371,7 @@ receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ
|
||||
where
|
||||
write q t = atomically $ writeTBQueue q t
|
||||
|
||||
send :: Transport c => THandleNTF c -> NtfServerClient -> IO ()
|
||||
send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO ()
|
||||
send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do
|
||||
t <- atomically $ readTBQueue sndQ
|
||||
void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)]
|
||||
@@ -381,7 +382,7 @@ send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do
|
||||
|
||||
data VerificationResult = VRVerified NtfRequest | VRFailed
|
||||
|
||||
verifyNtfTransmission :: Maybe (THandleAuth, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult
|
||||
verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult
|
||||
verifyNtfTransmission auth_ (tAuth, authorized, (corrId, entId, _)) cmd = do
|
||||
st <- asks store
|
||||
case cmd of
|
||||
@@ -441,7 +442,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
where
|
||||
processCommand :: NtfRequest -> M (Transmission NtfResponse)
|
||||
processCommand = \case
|
||||
NtfReqNew corrId (ANE SToken newTkn@(NewNtfTkn _ _ dhPubKey)) -> do
|
||||
NtfReqNew corrId (ANE SToken newTkn@(NewNtfTkn token _ dhPubKey)) -> do
|
||||
logDebug "TNEW - new token"
|
||||
st <- asks store
|
||||
ks@(srvDhPubKey, srvDhPrivKey) <- atomically . C.generateKeyPair =<< asks random
|
||||
@@ -452,9 +453,9 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
atomically $ addNtfToken st tknId tkn
|
||||
atomically $ writeTBQueue pushQ (tkn, PNVerification regCode)
|
||||
withNtfLog (`logCreateToken` tkn)
|
||||
incNtfStat tknCreated
|
||||
incNtfStatT token tknCreated
|
||||
pure (corrId, "", NRTknId tknId srvDhPubKey)
|
||||
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey), tknCronInterval}) (corrId, tknId, cmd) -> do
|
||||
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {token, ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey), tknCronInterval}) (corrId, tknId, cmd) -> do
|
||||
status <- readTVarIO tknStatus
|
||||
(corrId,tknId,) <$> case cmd of
|
||||
TNEW (NewNtfTkn _ _ dhPubKey) -> do
|
||||
@@ -473,7 +474,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
updateTknStatus tkn NTActive
|
||||
tIds <- atomically $ removeInactiveTokenRegistrations st tkn
|
||||
forM_ tIds cancelInvervalNotifications
|
||||
incNtfStat tknVerified
|
||||
incNtfStatT token tknVerified
|
||||
pure NROk
|
||||
| otherwise -> do
|
||||
logDebug "TVFY - incorrect code or token status"
|
||||
@@ -492,8 +493,8 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
addNtfToken st tknId tkn'
|
||||
writeTBQueue pushQ (tkn', PNVerification regCode)
|
||||
withNtfLog $ \s -> logUpdateToken s tknId token' regCode
|
||||
incNtfStat tknDeleted
|
||||
incNtfStat tknCreated
|
||||
incNtfStatT token tknDeleted
|
||||
incNtfStatT token tknCreated
|
||||
pure NROk
|
||||
TDEL -> do
|
||||
logDebug "TDEL"
|
||||
@@ -503,7 +504,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
atomically $ removeSubscription ca smpServer (SPNotifier, notifierId)
|
||||
cancelInvervalNotifications tknId
|
||||
withNtfLog (`logDeleteToken` tknId)
|
||||
incNtfStat tknDeleted
|
||||
incNtfStatT token tknDeleted
|
||||
pure NROk
|
||||
TCRN 0 -> do
|
||||
logDebug "TCRN 0"
|
||||
@@ -582,6 +583,10 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
withNtfLog :: (StoreLog 'WriteMode -> IO a) -> M ()
|
||||
withNtfLog action = liftIO . mapM_ action =<< asks storeLog
|
||||
|
||||
incNtfStatT :: DeviceToken -> (NtfServerStats -> TVar Int) -> M ()
|
||||
incNtfStatT (DeviceToken PPApnsNull _) _ = pure ()
|
||||
incNtfStatT _ statSel = incNtfStat statSel
|
||||
|
||||
incNtfStat :: (NtfServerStats -> TVar Int) -> M ()
|
||||
incNtfStat statSel = do
|
||||
stats <- asks serverStats
|
||||
|
||||
@@ -24,16 +24,16 @@ import Numeric.Natural
|
||||
import Simplex.Messaging.Client.Agent
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF)
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Notifications.Server.Stats
|
||||
import Simplex.Messaging.Notifications.Server.Store
|
||||
import Simplex.Messaging.Notifications.Server.StoreLog
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF)
|
||||
import Simplex.Messaging.Protocol (CorrId, SMPServer, Transmission)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport, THandleParams)
|
||||
import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
@@ -83,7 +83,7 @@ data NtfEnv = NtfEnv
|
||||
serverStats :: NtfServerStats
|
||||
}
|
||||
|
||||
newNtfServerEnv :: (MonadUnliftIO m, MonadRandom m) => NtfServerConfig -> m NtfEnv
|
||||
newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile} = do
|
||||
random <- liftIO C.newRandom
|
||||
store <- atomically newNtfStore
|
||||
@@ -161,13 +161,13 @@ data NtfRequest
|
||||
data NtfServerClient = NtfServerClient
|
||||
{ rcvQ :: TBQueue NtfRequest,
|
||||
sndQ :: TBQueue (Transmission NtfResponse),
|
||||
ntfThParams :: THandleParams NTFVersion,
|
||||
ntfThParams :: THandleParams NTFVersion 'TServer,
|
||||
connected :: TVar Bool,
|
||||
rcvActiveAt :: TVar SystemTime,
|
||||
sndActiveAt :: TVar SystemTime
|
||||
}
|
||||
|
||||
newNtfServerClient :: Natural -> THandleParams NTFVersion -> SystemTime -> STM NtfServerClient
|
||||
newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient
|
||||
newNtfServerClient qSize ntfThParams ts = do
|
||||
rcvQ <- newTBQueue qSize
|
||||
sndQ <- newTBQueue qSize
|
||||
|
||||
@@ -13,8 +13,7 @@ import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..))
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Server (runNtfServer)
|
||||
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration)
|
||||
@@ -31,9 +30,6 @@ import System.FilePath (combine)
|
||||
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
defaultSMPBatchDelay :: Int
|
||||
defaultSMPBatchDelay = 10000
|
||||
|
||||
ntfServerCLI :: FilePath -> FilePath -> IO ()
|
||||
ntfServerCLI cfgPath logPath =
|
||||
getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case
|
||||
@@ -87,9 +83,7 @@ ntfServerCLI cfgPath logPath =
|
||||
\# host is only used to print server address on start\n"
|
||||
<> ("host: " <> host <> "\n")
|
||||
<> ("port: " <> defaultServerPort <> "\n")
|
||||
<> "log_tls_errors: off\n\
|
||||
\# delay between command batches sent to SMP relays (microseconds), 0 to disable\n"
|
||||
<> ("smp_batch_delay: " <> show defaultSMPBatchDelay <> "\n")
|
||||
<> "log_tls_errors: off\n"
|
||||
<> "websockets: off\n\n\
|
||||
\[INACTIVE_CLIENTS]\n\
|
||||
\# TTL and interval to check inactive clients\n\
|
||||
@@ -111,8 +105,6 @@ ntfServerCLI cfgPath logPath =
|
||||
enableStoreLog = settingIsOn "STORE_LOG" "enable" ini
|
||||
logStats = settingIsOn "STORE_LOG" "log_stats" ini
|
||||
c = combine cfgPath . ($ defaultX509Config)
|
||||
smpBatchDelay = readIniDefault defaultSMPBatchDelay "TRANSPORT" "smp_batch_delay" ini
|
||||
batchDelay = if smpBatchDelay <= 0 then Nothing else Just smpBatchDelay
|
||||
serverConfig =
|
||||
NtfServerConfig
|
||||
{ transports = iniTransports ini,
|
||||
@@ -121,7 +113,7 @@ ntfServerCLI cfgPath logPath =
|
||||
clientQSize = 64,
|
||||
subQSize = 512,
|
||||
pushQSize = 1048,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {batchDelay}},
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
apnsConfig = defaultAPNSPushClientConfig,
|
||||
subsBatchSize = 900,
|
||||
inactiveClientExpiration =
|
||||
|
||||
@@ -54,7 +54,7 @@ import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Protocol (EncNMsgMeta)
|
||||
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..))
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
|
||||
import System.Environment (getEnv)
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -258,11 +258,11 @@ mkApnsJWTToken appTeamId jwtHeader privateKey = do
|
||||
connectHTTPS2 :: HostName -> APNSPushClientConfig -> TVar (Maybe HTTP2Client) -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
connectHTTPS2 apnsHost APNSPushClientConfig {apnsPort, http2cfg, caStoreFile} https2Client = do
|
||||
caStore_ <- XS.readCertificateStore caStoreFile
|
||||
when (isNothing caStore_) $ putStrLn $ "Error loading CertificateStore from " <> caStoreFile
|
||||
when (isNothing caStore_) $ logError $ "Error loading CertificateStore from " <> T.pack caStoreFile
|
||||
r <- getHTTP2Client apnsHost apnsPort caStore_ http2cfg disconnected
|
||||
case r of
|
||||
Right client -> atomically . writeTVar https2Client $ Just client
|
||||
Left e -> putStrLn $ "Error connecting to APNS: " <> show e
|
||||
Left e -> logError $ "Error connecting to APNS: " <> tshow e
|
||||
pure r
|
||||
where
|
||||
disconnected = atomically $ writeTVar https2Client Nothing
|
||||
|
||||
@@ -24,9 +24,12 @@ module Simplex.Messaging.Notifications.Server.StoreLog
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import qualified Data.Text as T
|
||||
import Data.Word (Word16)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -34,7 +37,7 @@ import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Store
|
||||
import Simplex.Messaging.Protocol (NtfPrivateAuthKey)
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (whenM)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, whenM)
|
||||
import System.Directory (doesFileExist, renameFile)
|
||||
import System.IO
|
||||
|
||||
@@ -189,10 +192,10 @@ readWriteNtfStore f st = do
|
||||
pure s
|
||||
|
||||
readNtfStore :: FilePath -> NtfStore -> IO ()
|
||||
readNtfStore f st = mapM_ addNtfLogRecord . B.lines =<< B.readFile f
|
||||
readNtfStore f st = mapM_ (addNtfLogRecord . LB.toStrict) . LB.lines =<< LB.readFile f
|
||||
where
|
||||
addNtfLogRecord s = case strDecode s of
|
||||
Left e -> B.putStrLn $ "Log parsing error (" <> B.pack e <> "): " <> B.take 100 s
|
||||
Left e -> logError $ "Log parsing error (" <> T.pack e <> "): " <> safeDecodeUtf8 (B.take 100 s)
|
||||
Right lr -> atomically $ case lr of
|
||||
CreateToken r@NtfTknRec {ntfTknId} -> do
|
||||
tkn <- mkTknData r
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Transport where
|
||||
|
||||
@@ -18,9 +19,9 @@ import qualified Data.X509 as X
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (liftEitherWith)
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
import Simplex.Messaging.Util (liftEitherWith)
|
||||
|
||||
ntfBlockSize :: Int
|
||||
ntfBlockSize = 512
|
||||
@@ -54,7 +55,7 @@ supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVers
|
||||
supportedServerNTFVRange :: VersionRangeNTF
|
||||
supportedServerNTFVRange = mkVersionRange initialNTFVersion currentServerNTFVersion
|
||||
|
||||
type THandleNTF c = THandle NTFVersion c
|
||||
type THandleNTF c p = THandle NTFVersion c p
|
||||
|
||||
data NtfServerHandshake = NtfServerHandshake
|
||||
{ ntfVersionRange :: VersionRangeNTF,
|
||||
@@ -67,9 +68,7 @@ data NtfClientHandshake = NtfClientHandshake
|
||||
{ -- | agreed SMP notifications server protocol version
|
||||
ntfVersion :: VersionNTF,
|
||||
-- | server identity - CA certificate fingerprint
|
||||
keyHash :: C.KeyHash,
|
||||
-- pub key to agree shared secret for entity ID encryption, shared secret for command authorization is agreed using per-queue keys.
|
||||
authPubKey :: Maybe C.PublicKeyX25519
|
||||
keyHash :: C.KeyHash
|
||||
}
|
||||
|
||||
instance Encoding NtfServerHandshake where
|
||||
@@ -94,62 +93,61 @@ authEncryptCmdsP :: VersionNTF -> Parser a -> Parser (Maybe a)
|
||||
authEncryptCmdsP v p = if v >= authBatchCmdsNTFVersion then Just <$> p else pure Nothing
|
||||
|
||||
instance Encoding NtfClientHandshake where
|
||||
smpEncode NtfClientHandshake {ntfVersion, keyHash, authPubKey} =
|
||||
smpEncode (ntfVersion, keyHash) <> encodeNtfAuthPubKey ntfVersion authPubKey
|
||||
smpEncode NtfClientHandshake {ntfVersion, keyHash} =
|
||||
smpEncode (ntfVersion, keyHash)
|
||||
smpP = do
|
||||
(ntfVersion, keyHash) <- smpP
|
||||
-- TODO drop SMP v6: remove special parser and make key non-optional
|
||||
authPubKey <- ntfAuthPubKeyP ntfVersion
|
||||
pure NtfClientHandshake {ntfVersion, keyHash, authPubKey}
|
||||
|
||||
ntfAuthPubKeyP :: VersionNTF -> Parser (Maybe C.PublicKeyX25519)
|
||||
ntfAuthPubKeyP v = if v >= authBatchCmdsNTFVersion then Just <$> smpP else pure Nothing
|
||||
|
||||
encodeNtfAuthPubKey :: VersionNTF -> Maybe C.PublicKeyX25519 -> ByteString
|
||||
encodeNtfAuthPubKey v k
|
||||
| v >= authBatchCmdsNTFVersion = maybe "" smpEncode k
|
||||
| otherwise = ""
|
||||
pure NtfClientHandshake {ntfVersion, keyHash}
|
||||
|
||||
-- | Notifcations server transport handshake.
|
||||
ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c)
|
||||
ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TServer)
|
||||
ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do
|
||||
let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c
|
||||
let sk = C.signX509 serverSignKey $ C.publicToX509 k
|
||||
sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange = ntfVRange, authPubKey = Just sk}
|
||||
getHandshake th >>= \case
|
||||
NtfClientHandshake {ntfVersion = v, keyHash, authPubKey = k'}
|
||||
NtfClientHandshake {ntfVersion = v, keyHash}
|
||||
| keyHash /= kh ->
|
||||
throwError $ TEHandshake IDENTITY
|
||||
| v `isCompatible` ntfVRange ->
|
||||
pure $ ntfThHandle th v pk k'
|
||||
pure $ ntfThHandleServer th v pk
|
||||
| otherwise -> throwError $ TEHandshake VERSION
|
||||
|
||||
-- | Notifcations server client transport handshake.
|
||||
ntfClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c)
|
||||
ntfClientHandshake c (k, pk) keyHash ntfVRange = do
|
||||
ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient)
|
||||
ntfClientHandshake c keyHash ntfVRange = do
|
||||
let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c
|
||||
NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th
|
||||
if sessionId /= sessId
|
||||
then throwError TEBadSession
|
||||
else case ntfVersionRange `compatibleVersion` ntfVRange of
|
||||
Just (Compatible v) -> do
|
||||
sk_ <- forM sk' $ \exact -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
|
||||
ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
|
||||
serverKey <- getServerVerifyKey c
|
||||
pubKey <- C.verifyX509 serverKey exact
|
||||
C.x509ToPublic (pubKey, []) >>= C.pubKey
|
||||
sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash, authPubKey = Just k}
|
||||
pure $ ntfThHandle th v pk sk_
|
||||
pubKey <- C.verifyX509 serverKey signedKey
|
||||
(,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
|
||||
sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash}
|
||||
pure $ ntfThHandleClient th v ck_
|
||||
Nothing -> throwError $ TEHandshake VERSION
|
||||
|
||||
ntfThHandle :: forall c. THandleNTF c -> VersionNTF -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleNTF c
|
||||
ntfThHandle th@THandle {params} v privKey k_ =
|
||||
-- TODO drop SMP v6: make thAuth non-optional
|
||||
let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_
|
||||
v3 = v >= authBatchCmdsNTFVersion
|
||||
params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3}
|
||||
in (th :: THandleNTF c) {params = params'}
|
||||
ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer
|
||||
ntfThHandleServer th v pk =
|
||||
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing}
|
||||
in ntfThHandle_ th v (Just thAuth)
|
||||
|
||||
ntfTHandle :: Transport c => c -> THandleNTF c
|
||||
ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient
|
||||
ntfThHandleClient th v ck_ =
|
||||
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = Nothing}) <$> ck_
|
||||
in ntfThHandle_ th v thAuth
|
||||
|
||||
ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p
|
||||
ntfThHandle_ th@THandle {params} v thAuth =
|
||||
-- TODO drop SMP v6: make thAuth non-optional
|
||||
let v3 = v >= authBatchCmdsNTFVersion
|
||||
params' = params {thVersion = v, 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}
|
||||
|
||||
@@ -43,6 +43,8 @@ module Simplex.Messaging.Protocol
|
||||
( -- * SMP protocol parameters
|
||||
supportedSMPClientVRange,
|
||||
maxMessageLength,
|
||||
paddedProxiedMsgLength,
|
||||
paddedForwardedMsgLength,
|
||||
e2eEncConfirmationLength,
|
||||
e2eEncMessageLength,
|
||||
|
||||
@@ -56,6 +58,7 @@ module Simplex.Messaging.Protocol
|
||||
SubscriptionMode (..),
|
||||
Party (..),
|
||||
Cmd (..),
|
||||
DirectParty,
|
||||
BrokerMsg (..),
|
||||
SParty (..),
|
||||
PartyI (..),
|
||||
@@ -63,6 +66,7 @@ module Simplex.Messaging.Protocol
|
||||
ProtocolErrorType (..),
|
||||
ErrorType (..),
|
||||
CommandError (..),
|
||||
ProxyError (..),
|
||||
Transmission,
|
||||
TransmissionAuth (..),
|
||||
SignedTransmission,
|
||||
@@ -121,6 +125,12 @@ module Simplex.Messaging.Protocol
|
||||
EncNMsgMeta,
|
||||
SMPMsgMeta (..),
|
||||
NMsgMeta (..),
|
||||
EncFwdResponse (..),
|
||||
EncFwdTransmission (..),
|
||||
EncResponse (..),
|
||||
EncTransmission (..),
|
||||
FwdResponse (..),
|
||||
FwdTransmission (..),
|
||||
MsgFlags (..),
|
||||
initialSMPClientVersion,
|
||||
userProtocol,
|
||||
@@ -185,10 +195,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 +212,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
|
||||
|
||||
@@ -231,6 +246,20 @@ supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClie
|
||||
maxMessageLength :: Int
|
||||
maxMessageLength = 16088
|
||||
|
||||
-- without signature works with min 16151 (fails with 16150)
|
||||
-- with Ed448: 16265 (fails with 16264)
|
||||
-- with Ed25519: 16215 (fails with 16214)
|
||||
-- with X25519: 16232 (fails with 16231)
|
||||
paddedProxiedMsgLength :: Int
|
||||
paddedProxiedMsgLength = 16232
|
||||
|
||||
-- without signature works with min 16239 (fails with 16238)
|
||||
-- with Ed448: 16353 (fails with 16352)
|
||||
-- with Ed25519: 16303 (fails with 16302)
|
||||
-- with X25519: 16320 (fails with 16319)
|
||||
paddedForwardedMsgLength :: Int
|
||||
paddedForwardedMsgLength = 16320
|
||||
|
||||
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 +273,7 @@ e2eEncMessageLength :: Int
|
||||
e2eEncMessageLength = 16032
|
||||
|
||||
-- | SMP protocol clients
|
||||
data Party = Recipient | Sender | Notifier
|
||||
data Party = Recipient | Sender | Notifier | ProxiedClient
|
||||
deriving (Show)
|
||||
|
||||
-- | Singleton types for SMP protocol clients
|
||||
@@ -252,11 +281,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 +300,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 +399,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 :: 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 +435,25 @@ instance Encoding SubscriptionMode where
|
||||
'C' -> pure SMOnlyCreate
|
||||
_ -> fail "bad SubscriptionMode"
|
||||
|
||||
newtype EncTransmission = EncTransmission ByteString
|
||||
deriving (Show)
|
||||
|
||||
data FwdTransmission = FwdTransmission
|
||||
{ fwdCorrId :: CorrId,
|
||||
fwdKey :: C.PublicKeyX25519,
|
||||
fwdTransmission :: EncTransmission
|
||||
}
|
||||
|
||||
instance Encoding FwdTransmission where
|
||||
smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdKey, fwdTransmission = EncTransmission t} =
|
||||
smpEncode (corrId, fwdKey, Tail t)
|
||||
smpP = do
|
||||
(corrId, fwdKey, Tail t) <- smpP
|
||||
pure FwdTransmission {fwdCorrId = CorrId corrId, 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 +463,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 +479,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 +659,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 +675,9 @@ data BrokerMsgTag
|
||||
| MSG_
|
||||
| NID_
|
||||
| NMSG_
|
||||
| PKEY_
|
||||
| RRES_
|
||||
| PRES_
|
||||
| END_
|
||||
| OK_
|
||||
| ERR_
|
||||
@@ -607,6 +705,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 +724,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 +743,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 +758,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 +939,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,6 +1148,8 @@ 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
|
||||
| -- | SMP queue capacity is exceeded on the server
|
||||
@@ -1046,6 +1158,8 @@ data ErrorType
|
||||
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 +1169,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,8 +1192,23 @@ 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 `ERR SMP err`.
|
||||
PROTOCOL {protocolErr :: ErrorType}
|
||||
| -- | Invalid server response that failed to parse.
|
||||
-- Forwarded to the agent client as `ERR BROKER RESPONSE`.
|
||||
RESPONSE {responseErr :: ErrorType}
|
||||
| UNEXPECTED {unexpectedResponse :: String} -- 'String' for using derived JSON and Arbitrary instances
|
||||
| TIMEOUT
|
||||
| NETWORK
|
||||
| BAD_HOST
|
||||
| NO_SESSION
|
||||
| TRANSPORT {transportErr :: TransportError}
|
||||
deriving (Eq, Read, Show)
|
||||
|
||||
-- | SMP transmission parser.
|
||||
transmissionP :: THandleParams v -> Parser RawTransmission
|
||||
transmissionP :: THandleParams v p -> Parser RawTransmission
|
||||
transmissionP THandleParams {sessionId, implySessId} = do
|
||||
authenticator <- smpP
|
||||
authorized <- A.takeByteString
|
||||
@@ -1089,10 +1222,10 @@ transmissionP THandleParams {sessionId, implySessId} = do
|
||||
command <- A.takeByteString
|
||||
pure RawTransmission {authenticator, authorized = authorized', sessId, corrId, entityId, command}
|
||||
|
||||
class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where
|
||||
class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where
|
||||
type ProtoCommand msg = cmd | cmd -> msg
|
||||
type ProtoType msg = (sch :: ProtocolType) | sch -> msg
|
||||
protocolClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c)
|
||||
protocolClientHandshake :: forall c. Transport c => c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c 'TClient)
|
||||
protocolPing :: ProtoCommand msg
|
||||
protocolError :: msg -> Maybe err
|
||||
|
||||
@@ -1135,6 +1268,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 pubKey (EncTransmission s) -> e (PFWD_, ' ', pubKey, Tail s)
|
||||
RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s)
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
@@ -1144,24 +1280,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 +1334,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 <*> (EncTransmission . unTail <$> smpP)
|
||||
PRXY_ -> PRXY <$> _smpP <*> smpP
|
||||
CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB
|
||||
|
||||
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
||||
@@ -1204,6 +1354,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 +1374,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 +1389,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 +1429,10 @@ instance Encoding ErrorType where
|
||||
BLOCK -> "BLOCK"
|
||||
SESSION -> "SESSION"
|
||||
CMD err -> "CMD " <> smpEncode err
|
||||
PROXY err -> "PROXY " <> smpEncode err
|
||||
AUTH -> "AUTH"
|
||||
QUOTA -> "QUOTA"
|
||||
EXPIRED -> "EXPIRED"
|
||||
NO_MSG -> "NO_MSG"
|
||||
LARGE_MSG -> "LARGE_MSG"
|
||||
INTERNAL -> "INTERNAL"
|
||||
@@ -1280,8 +1443,10 @@ instance Encoding ErrorType where
|
||||
"BLOCK" -> pure BLOCK
|
||||
"SESSION" -> pure SESSION
|
||||
"CMD" -> CMD <$> _smpP
|
||||
"PROXY" -> PROXY <$> _smpP
|
||||
"AUTH" -> pure AUTH
|
||||
"QUOTA" -> pure QUOTA
|
||||
"EXPIRED" -> pure EXPIRED
|
||||
"NO_MSG" -> pure NO_MSG
|
||||
"LARGE_MSG" -> pure LARGE_MSG
|
||||
"INTERNAL" -> pure INTERNAL
|
||||
@@ -1304,11 +1469,49 @@ instance Encoding CommandError where
|
||||
"NO_AUTH" -> pure NO_AUTH
|
||||
"HAS_AUTH" -> pure HAS_AUTH
|
||||
"NO_ENTITY" -> pure NO_ENTITY
|
||||
"NO_QUEUE" -> pure NO_ENTITY
|
||||
"NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility
|
||||
_ -> fail "bad command error type"
|
||||
|
||||
instance Encoding ProxyError where
|
||||
smpEncode e = case e of
|
||||
PROTOCOL et -> "PROTOCOL " <> smpEncode et
|
||||
RESPONSE et -> "RESPONSE " <> smpEncode et
|
||||
UNEXPECTED s -> "UNEXPECTED " <> smpEncode (encodeUtf8 $ T.pack s)
|
||||
TIMEOUT -> "TIMEOUT"
|
||||
NETWORK -> "NETWORK"
|
||||
BAD_HOST -> "BAD_HOST"
|
||||
NO_SESSION -> "NO_SESSION"
|
||||
TRANSPORT t -> "TRANSPORT " <> serializeTransportError t
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"PROTOCOL" -> PROTOCOL <$> _smpP
|
||||
"RESPONSE" -> RESPONSE <$> _smpP
|
||||
"UNEXPECTED" -> UNEXPECTED . (T.unpack . safeDecodeUtf8) <$> _smpP
|
||||
"TIMEOUT" -> pure TIMEOUT
|
||||
"NETWORK" -> pure NETWORK
|
||||
"BAD_HOST" -> pure BAD_HOST
|
||||
"NO_SESSION" -> pure NO_SESSION
|
||||
"TRANSPORT" -> TRANSPORT <$> (A.space *> transportErrorP)
|
||||
_ -> fail "bad command error type"
|
||||
|
||||
instance StrEncoding ProxyError where
|
||||
strEncode = \case
|
||||
PROTOCOL et -> "PROTOCOL " <> strEncode et
|
||||
RESPONSE et -> "RESPONSE " <> strEncode et
|
||||
UNEXPECTED "" -> "UNEXPECTED" -- Arbitrary instance generates empty strings which String instance can't handle
|
||||
UNEXPECTED s -> "UNEXPECTED " <> strEncode s
|
||||
TRANSPORT t -> "TRANSPORT " <> serializeTransportError t
|
||||
e -> bshow e
|
||||
strP =
|
||||
"PROTOCOL " *> (PROTOCOL <$> strP)
|
||||
<|> "RESPONSE " *> (RESPONSE <$> strP)
|
||||
<|> "UNEXPECTED " *> (UNEXPECTED <$> strP)
|
||||
<|> "UNEXPECTED" $> UNEXPECTED ""
|
||||
<|> "TRANSPORT " *> (TRANSPORT <$> transportErrorP)
|
||||
<|> parseRead1
|
||||
|
||||
-- | Send signed SMP transmission to TCP transport.
|
||||
tPut :: Transport c => THandle v c -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()]
|
||||
tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()]
|
||||
tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (batch params) (blockSize params)
|
||||
where
|
||||
tPutBatch :: TransportBatch () -> IO [Either TransportError ()]
|
||||
@@ -1317,7 +1520,7 @@ tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (ba
|
||||
TBTransmissions s n _ -> replicate n <$> tPutLog th s
|
||||
TBTransmission s _ -> (: []) <$> tPutLog th s
|
||||
|
||||
tPutLog :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ())
|
||||
tPutLog :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ())
|
||||
tPutLog th s = do
|
||||
r <- tPutBlock th s
|
||||
case r of
|
||||
@@ -1383,7 +1586,7 @@ tEncodeBatch1 t = lenEncode 1 `B.cons` tEncodeForBatch t
|
||||
-- tForAuth is lazy to avoid computing it when there is no key to sign
|
||||
data TransmissionForAuth = TransmissionForAuth {tForAuth :: ~ByteString, tToSend :: ByteString}
|
||||
|
||||
encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> TransmissionForAuth
|
||||
encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> TransmissionForAuth
|
||||
encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t =
|
||||
TransmissionForAuth {tForAuth, tToSend = if implySessId then t' else tForAuth}
|
||||
where
|
||||
@@ -1391,7 +1594,7 @@ encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId}
|
||||
t' = encodeTransmission_ v t
|
||||
{-# INLINE encodeTransmissionForAuth #-}
|
||||
|
||||
encodeTransmission :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> ByteString
|
||||
encodeTransmission :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> ByteString
|
||||
encodeTransmission THandleParams {thVersion = v, sessionId, implySessId} t =
|
||||
if implySessId then t' else smpEncode sessionId <> t'
|
||||
where
|
||||
@@ -1404,11 +1607,11 @@ encodeTransmission_ v (CorrId corrId, queueId, command) =
|
||||
{-# INLINE encodeTransmission_ #-}
|
||||
|
||||
-- | Receive and parse transmission from the TCP transport (ignoring any trailing padding).
|
||||
tGetParse :: Transport c => THandle v c -> IO (NonEmpty (Either TransportError RawTransmission))
|
||||
tGetParse :: Transport c => THandle v c p -> IO (NonEmpty (Either TransportError RawTransmission))
|
||||
tGetParse th@THandle {params} = eitherList (tParse params) <$> tGetBlock th
|
||||
{-# INLINE tGetParse #-}
|
||||
|
||||
tParse :: THandleParams v -> ByteString -> NonEmpty (Either TransportError RawTransmission)
|
||||
tParse :: THandleParams v p -> ByteString -> NonEmpty (Either TransportError RawTransmission)
|
||||
tParse thParams@THandleParams {batch} s
|
||||
| batch = eitherList (L.map (\(Large t) -> tParse1 t)) ts
|
||||
| otherwise = [tParse1 s]
|
||||
@@ -1420,10 +1623,10 @@ eitherList :: (a -> NonEmpty (Either e b)) -> Either e a -> NonEmpty (Either e b
|
||||
eitherList = either (\e -> [Left e])
|
||||
|
||||
-- | Receive client and server transmissions (determined by `cmd` type).
|
||||
tGet :: forall v err cmd c. (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (NonEmpty (SignedTransmission err cmd))
|
||||
tGet :: forall v err cmd c p. (ProtocolEncoding v err cmd, Transport c) => THandle v c p -> IO (NonEmpty (SignedTransmission err cmd))
|
||||
tGet th@THandle {params} = L.map (tDecodeParseValidate params) <$> tGetParse th
|
||||
|
||||
tDecodeParseValidate :: forall v err cmd. ProtocolEncoding v err cmd => THandleParams v -> Either TransportError RawTransmission -> SignedTransmission err cmd
|
||||
tDecodeParseValidate :: forall v p err cmd. ProtocolEncoding v err cmd => THandleParams v p -> Either TransportError RawTransmission -> SignedTransmission err cmd
|
||||
tDecodeParseValidate THandleParams {sessionId, thVersion = v, implySessId} = \case
|
||||
Right RawTransmission {authenticator, authorized, sessId, corrId, entityId, command}
|
||||
| implySessId || sessId == sessionId ->
|
||||
@@ -1444,4 +1647,5 @@ $(J.deriveJSON defaultJSON ''MsgFlags)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''CommandError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''ErrorType)
|
||||
-- run deriveJSON in one TH splice to allow mutual instance
|
||||
$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType])
|
||||
|
||||
+225
-93
@@ -13,7 +13,6 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Server
|
||||
@@ -43,16 +42,19 @@ 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)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Either (fromRight, partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import Data.List (intercalate)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (isNothing)
|
||||
@@ -66,8 +68,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 (PCEIOError), forwardSMPMessage, smpProxyError)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgent (..), SMPClientAgentEvent (..), getSMPServerClient', 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
|
||||
@@ -89,6 +93,7 @@ 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
|
||||
@@ -121,21 +126,24 @@ 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)
|
||||
where
|
||||
runServer :: (ServiceName, ATransport) -> M ()
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
serverParams <- asks tlsServerParams
|
||||
ss <- asks sockets
|
||||
serverSignKey <- either fail pure . fromTLSCredentials $ tlsServerCredentials serverParams
|
||||
runTransportServerState ss started tcpPort serverParams tCfg (runClient serverSignKey t)
|
||||
env <- ask
|
||||
liftIO $ runTransportServerState ss started tcpPort serverParams tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
|
||||
fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey
|
||||
|
||||
saveServer :: Bool -> M ()
|
||||
@@ -177,6 +185,19 @@ 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)
|
||||
CAReconnected srv -> logInfo $ "SMP server reconnected " <> showServer' srv
|
||||
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_ _ = []
|
||||
@@ -284,20 +305,35 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
hSetBuffering h LineBuffering
|
||||
hSetNewlineMode h universalNewlineMode
|
||||
hPutStrLn h "SMP server control port\n'help' for supported commands"
|
||||
cpLoop h
|
||||
role <- newTVarIO CPRNone
|
||||
cpLoop h role
|
||||
where
|
||||
cpLoop h = do
|
||||
s <- B.hGetLine h
|
||||
case strDecode $ trimCR s of
|
||||
cpLoop h role = do
|
||||
s <- trimCR <$> B.hGetLine h
|
||||
case strDecode s of
|
||||
Right CPQuit -> hClose h
|
||||
Right cmd -> processCP h cmd >> cpLoop h
|
||||
Left err -> hPutStrLn h ("error: " <> err) >> cpLoop h
|
||||
processCP h = \case
|
||||
CPSuspend -> hPutStrLn h "suspend not implemented"
|
||||
CPResume -> hPutStrLn h "resume not implemented"
|
||||
CPClients -> do
|
||||
Right cmd -> logCmd s cmd >> processCP h role cmd >> cpLoop h role
|
||||
Left err -> hPutStrLn h ("error: " <> err) >> cpLoop h role
|
||||
logCmd s cmd = when shouldLog $ logWarn $ "ControlPort: " <> tshow s
|
||||
where
|
||||
shouldLog = case cmd of
|
||||
CPAuth _ -> False
|
||||
CPHelp -> False
|
||||
CPQuit -> False
|
||||
CPSkip -> False
|
||||
_ -> True
|
||||
processCP h role = \case
|
||||
CPAuth auth -> atomically $ writeTVar role $! newRole cfg
|
||||
where
|
||||
newRole ServerConfig {controlPortUserAuth = user, controlPortAdminAuth = admin}
|
||||
| Just auth == admin = CPRAdmin
|
||||
| Just auth == user = CPRUser
|
||||
| otherwise = CPRNone
|
||||
CPSuspend -> withAdminRole $ hPutStrLn h "suspend not implemented"
|
||||
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
|
||||
@@ -306,7 +342,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
let age = systemSeconds now - systemSeconds createdAt
|
||||
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 -> do
|
||||
CPStats -> withAdminRole $ do
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgSentNtf, msgRecvNtf, qCount, msgCount} <- unliftIO u $ asks serverStats
|
||||
putStat "fromTime" fromTime
|
||||
putStat "qCreated" qCreated
|
||||
@@ -324,7 +360,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
putStat :: Show a => String -> TVar a -> IO ()
|
||||
putStat label var = readTVarIO var >>= \v -> hPutStrLn h $ label <> ": " <> show v
|
||||
CPStatsRTS -> getRTSStats >>= hPrint h
|
||||
CPThreads -> do
|
||||
CPThreads -> withAdminRole $ do
|
||||
#if MIN_VERSION_base(4,18,0)
|
||||
threads <- liftIO listThreads
|
||||
hPutStrLn h $ "Threads: " <> show (length threads)
|
||||
@@ -335,7 +371,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
#else
|
||||
hPutStrLn h "Not available on GHC 8.10"
|
||||
#endif
|
||||
CPSockets -> do
|
||||
CPSockets -> withAdminRole $ do
|
||||
(accepted', closed', active') <- unliftIO u $ asks sockets
|
||||
(accepted, closed, active) <- atomically $ (,,) <$> readTVar accepted' <*> readTVar closed' <*> readTVar active'
|
||||
hPutStrLn h "Sockets: "
|
||||
@@ -343,7 +379,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
hPutStrLn h $ "closed: " <> show closed
|
||||
hPutStrLn h $ "active: " <> show (IM.size active)
|
||||
hPutStrLn h $ "leaked: " <> show (accepted - closed - IM.size active)
|
||||
CPSocketThreads -> do
|
||||
CPSocketThreads -> withAdminRole $ do
|
||||
#if MIN_VERSION_base(4,18,0)
|
||||
(_, _, active') <- unliftIO u $ asks sockets
|
||||
active <- readTVarIO active'
|
||||
@@ -357,7 +393,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
#else
|
||||
hPutStrLn h "Not available on GHC 8.10"
|
||||
#endif
|
||||
CPDelete queueId' -> unliftIO u $ do
|
||||
CPDelete queueId' -> withUserRole $ unliftIO u $ do
|
||||
st <- asks queueStore
|
||||
ms <- asks msgStore
|
||||
queueId <- atomically (getQueue st SSender queueId') >>= \case
|
||||
@@ -372,16 +408,28 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
withLog (`logDeleteQueue` queueId)
|
||||
updateDeletedStats q
|
||||
liftIO . hPutStrLn h $ "ok, " <> show numDeleted <> " messages deleted"
|
||||
CPSave -> withLock (savingLock srv) "control" $ do
|
||||
CPSave -> withAdminRole $ withLock' (savingLock srv) "control" $ do
|
||||
hPutStrLn h "saving server state..."
|
||||
unliftIO u $ saveServer True
|
||||
hPutStrLn h "server state saved!"
|
||||
CPHelp -> hPutStrLn h "commands: stats, stats-rts, clients, sockets, socket-threads, threads, delete, save, help, quit"
|
||||
CPQuit -> pure ()
|
||||
CPSkip -> pure ()
|
||||
where
|
||||
withUserRole action = readTVarIO role >>= \case
|
||||
CPRAdmin -> action
|
||||
CPRUser -> action
|
||||
_ -> do
|
||||
logError "Unauthorized control port command"
|
||||
hPutStrLn h "AUTH"
|
||||
withAdminRole action = readTVarIO role >>= \case
|
||||
CPRAdmin -> action
|
||||
_ -> do
|
||||
logError "Unauthorized control port command"
|
||||
hPutStrLn h "AUTH"
|
||||
|
||||
runClientTransport :: Transport c => THandleSMP c -> M ()
|
||||
runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do
|
||||
runClientTransport :: Transport c => THandleSMP c 'TServer -> M ()
|
||||
runClientTransport th@THandle {params = thParams@THandleParams {thVersion, sessionId}} = do
|
||||
q <- asks $ tbqSize . config
|
||||
ts <- liftIO getSystemTime
|
||||
active <- asks clients
|
||||
@@ -393,7 +441,7 @@ runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} =
|
||||
s <- asks server
|
||||
expCfg <- asks $ inactiveClientExpiration . config
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId
|
||||
raceAny_ ([liftIO $ send th c, client c s, receive th c] <> disconnectThread_ c expCfg)
|
||||
raceAny_ ([liftIO $ send th c, client thParams c s, receive th c] <> disconnectThread_ c expCfg)
|
||||
`finally` clientDisconnected c
|
||||
where
|
||||
disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport th (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)]
|
||||
@@ -428,28 +476,28 @@ cancelSub sub =
|
||||
Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread
|
||||
_ -> return ()
|
||||
|
||||
receive :: Transport c => THandleSMP c -> Client -> M ()
|
||||
receive :: Transport c => THandleSMP c 'TServer -> Client -> M ()
|
||||
receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive"
|
||||
forever $ do
|
||||
ts <- L.toList <$> liftIO (tGet th)
|
||||
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 => THandleSMP c -> Client -> IO ()
|
||||
send :: Transport c => THandleSMP c 'TServer -> Client -> IO ()
|
||||
send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send"
|
||||
forever $ do
|
||||
@@ -464,7 +512,7 @@ send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do
|
||||
NMSG {} -> 0
|
||||
_ -> 1
|
||||
|
||||
disconnectTransport :: Transport c => THandle v c -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO ()
|
||||
disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO ()
|
||||
disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " disconnectTransport"
|
||||
loop
|
||||
@@ -485,7 +533,7 @@ data VerificationResult = VRVerified (Maybe QueueRec) | VRFailed
|
||||
-- - the queue or party key do not exist.
|
||||
-- In all cases, the time of the verification should depend only on the provided authorization type,
|
||||
-- a dummy key is used to run verification in the last two cases, and failure is returned irrespective of the result.
|
||||
verifyTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult
|
||||
verifyTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult
|
||||
verifyTransmission auth_ tAuth authorized queueId cmd =
|
||||
case cmd of
|
||||
Cmd SRecipient (NEW k _ _ _) -> pure $ Nothing `verifiedWith` k
|
||||
@@ -493,21 +541,23 @@ 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
|
||||
|
||||
verifyCmdAuthorization :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool
|
||||
verifyCmdAuthorization :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool
|
||||
verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAuth
|
||||
where
|
||||
verify :: C.APublicAuthKey -> TransmissionAuth -> Bool
|
||||
@@ -519,12 +569,12 @@ verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAu
|
||||
C.SX25519 -> verifyCmdAuth auth_ k s authorized
|
||||
_ -> verifyCmdAuth auth_ dummyKeyX25519 s authorized `seq` False
|
||||
|
||||
verifyCmdAuth :: Maybe (THandleAuth, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool
|
||||
verifyCmdAuth :: Maybe (THandleAuth 'TServer, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool
|
||||
verifyCmdAuth auth_ k authenticator authorized = case auth_ of
|
||||
Just (THandleAuth {privKey}, nonce) -> C.cbVerify k privKey nonce authenticator authorized
|
||||
Just (THAuthServer {serverPrivKey = pk}, nonce) -> C.cbVerify k pk nonce authenticator authorized
|
||||
Nothing -> False
|
||||
|
||||
dummyVerifyCmd :: Maybe (THandleAuth, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool
|
||||
dummyVerifyCmd :: Maybe (THandleAuth 'TServer, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool
|
||||
dummyVerifyCmd auth_ authorized = \case
|
||||
TASignature (C.ASignature a s) -> C.verify' (dummySignKey a) s authorized
|
||||
TAAuthenticator s -> verifyCmdAuth auth_ dummyKeyX25519 s authorized
|
||||
@@ -552,25 +602,54 @@ dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/Xo
|
||||
dummyKeyX25519 :: C.PublicKey 'C.X25519
|
||||
dummyKeyX25519 = "MCowBQYDK2VuAyEA4JGSMYht18H4mas/jHeBwfcM7jLwNYJNOAhi2/g4RXg="
|
||||
|
||||
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => 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 AUTH)
|
||||
where
|
||||
allowProxy = do
|
||||
ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config
|
||||
pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth
|
||||
getRelay = do
|
||||
ProxyAgent {smpAgent} <- asks proxyAgent
|
||||
liftIO $ proxyResp <$> runExceptT (getSMPServerClient' smpAgent srv) `catch` (pure . Left . PCEIOError)
|
||||
where
|
||||
proxyResp = \case
|
||||
Right smp ->
|
||||
let THandleParams {sessionId = srvSessId, thAuth} = thParams smp
|
||||
vr = supportedServerSMPRelayVRange
|
||||
in case thAuth of
|
||||
Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey
|
||||
Nothing -> ERR $ PROXY (TRANSPORT TENoServerAuth)
|
||||
Left err -> ERR $ smpProxyError err
|
||||
PFWD pubKey encBlock -> do
|
||||
ProxyAgent {smpAgent} <- asks proxyAgent
|
||||
atomically (lookupSMPServerClient smpAgent sessId) >>= \case
|
||||
Just smp -> liftIO $ either (ERR . smpProxyError) PRES <$> runExceptT (forwardSMPMessage smp corrId pubKey encBlock)
|
||||
Nothing -> pure $ ERR $ PROXY NO_SESSION
|
||||
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
|
||||
@@ -589,7 +668,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
OFF -> suspendQueue_ st
|
||||
DEL -> delQueueAndMsgs st
|
||||
where
|
||||
createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> m (Transmission BrokerMsg)
|
||||
createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> M (Transmission BrokerMsg)
|
||||
createQueue st recipientKey dhKey subMode = time "NEW" $ do
|
||||
(rcvPublicDhKey, privDhKey) <- atomically . C.generateKeyPair =<< asks random
|
||||
let rcvDhSecret = C.dh' dhKey privDhKey
|
||||
@@ -607,7 +686,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
(corrId,queueId,) <$> addQueueRetry 3 qik qRec
|
||||
where
|
||||
addQueueRetry ::
|
||||
Int -> ((RecipientId, SenderId) -> QueueIdsKeys) -> ((RecipientId, SenderId) -> QueueRec) -> m BrokerMsg
|
||||
Int -> ((RecipientId, SenderId) -> QueueIdsKeys) -> ((RecipientId, SenderId) -> QueueRec) -> M BrokerMsg
|
||||
addQueueRetry 0 _ _ = pure $ ERR INTERNAL
|
||||
addQueueRetry n qik qRec = do
|
||||
ids@(rId, _) <- getIds
|
||||
@@ -632,25 +711,25 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
Right q -> logCreateQueue s q
|
||||
_ -> pure ()
|
||||
|
||||
getIds :: m (RecipientId, SenderId)
|
||||
getIds :: M (RecipientId, SenderId)
|
||||
getIds = do
|
||||
n <- asks $ queueIdBytes . config
|
||||
liftM2 (,) (randomId n) (randomId n)
|
||||
|
||||
secureQueue_ :: QueueStore -> SndPublicAuthKey -> m (Transmission BrokerMsg)
|
||||
secureQueue_ :: QueueStore -> SndPublicAuthKey -> M (Transmission BrokerMsg)
|
||||
secureQueue_ st sKey = time "KEY" $ do
|
||||
withLog $ \s -> logSecureQueue s queueId sKey
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (qSecured stats) (+ 1)
|
||||
atomically $ (corrId,queueId,) . either ERR (const OK) <$> secureQueue st queueId sKey
|
||||
|
||||
addQueueNotifier_ :: QueueStore -> NtfPublicAuthKey -> RcvNtfPublicDhKey -> m (Transmission BrokerMsg)
|
||||
addQueueNotifier_ :: QueueStore -> NtfPublicAuthKey -> RcvNtfPublicDhKey -> M (Transmission BrokerMsg)
|
||||
addQueueNotifier_ st notifierKey dhKey = time "NKEY" $ do
|
||||
(rcvPublicDhKey, privDhKey) <- atomically . C.generateKeyPair =<< asks random
|
||||
let rcvNtfDhSecret = C.dh' dhKey privDhKey
|
||||
(corrId,queueId,) <$> addNotifierRetry 3 rcvPublicDhKey rcvNtfDhSecret
|
||||
where
|
||||
addNotifierRetry :: Int -> RcvNtfPublicDhKey -> RcvNtfDhSecret -> m BrokerMsg
|
||||
addNotifierRetry :: Int -> RcvNtfPublicDhKey -> RcvNtfDhSecret -> M BrokerMsg
|
||||
addNotifierRetry 0 _ _ = pure $ ERR INTERNAL
|
||||
addNotifierRetry n rcvPublicDhKey rcvNtfDhSecret = do
|
||||
notifierId <- randomId =<< asks (queueIdBytes . config)
|
||||
@@ -662,17 +741,17 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
withLog $ \s -> logAddNotifier s queueId ntfCreds
|
||||
pure $ NID notifierId rcvPublicDhKey
|
||||
|
||||
deleteQueueNotifier_ :: QueueStore -> m (Transmission BrokerMsg)
|
||||
deleteQueueNotifier_ :: QueueStore -> M (Transmission BrokerMsg)
|
||||
deleteQueueNotifier_ st = do
|
||||
withLog (`logDeleteNotifier` queueId)
|
||||
okResp <$> atomically (deleteQueueNotifier st queueId)
|
||||
|
||||
suspendQueue_ :: QueueStore -> m (Transmission BrokerMsg)
|
||||
suspendQueue_ :: QueueStore -> M (Transmission BrokerMsg)
|
||||
suspendQueue_ st = do
|
||||
withLog (`logSuspendQueue` queueId)
|
||||
okResp <$> atomically (suspendQueue st queueId)
|
||||
|
||||
subscribeQueue :: QueueRec -> RecipientId -> m (Transmission BrokerMsg)
|
||||
subscribeQueue :: QueueRec -> RecipientId -> M (Transmission BrokerMsg)
|
||||
subscribeQueue qr rId = do
|
||||
atomically (TM.lookup rId subscriptions) >>= \case
|
||||
Nothing ->
|
||||
@@ -685,19 +764,19 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
s ->
|
||||
atomically (tryTakeTMVar $ delivered s) >> deliver sub
|
||||
where
|
||||
newSub :: m (TVar Sub)
|
||||
newSub :: M (TVar Sub)
|
||||
newSub = time "SUB newSub" . atomically $ do
|
||||
writeTQueue subscribedQ (rId, clnt)
|
||||
sub <- newTVar =<< newSubscription NoSub
|
||||
TM.insert rId sub subscriptions
|
||||
pure sub
|
||||
deliver :: TVar Sub -> m (Transmission BrokerMsg)
|
||||
deliver :: TVar Sub -> M (Transmission BrokerMsg)
|
||||
deliver sub = do
|
||||
q <- getStoreMsgQueue "SUB" rId
|
||||
msg_ <- atomically $ tryPeekMsg q
|
||||
deliverMessage "SUB" qr rId sub q msg_
|
||||
|
||||
getMessage :: QueueRec -> m (Transmission BrokerMsg)
|
||||
getMessage :: QueueRec -> M (Transmission BrokerMsg)
|
||||
getMessage qr = time "GET" $ do
|
||||
atomically (TM.lookup queueId subscriptions) >>= \case
|
||||
Nothing ->
|
||||
@@ -716,7 +795,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
sub <- newTVar s
|
||||
TM.insert queueId sub subscriptions
|
||||
pure s
|
||||
getMessage_ :: Sub -> m (Transmission BrokerMsg)
|
||||
getMessage_ :: Sub -> M (Transmission BrokerMsg)
|
||||
getMessage_ s = do
|
||||
q <- getStoreMsgQueue "GET" queueId
|
||||
atomically $
|
||||
@@ -726,17 +805,17 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
in setDelivered s msg $> (corrId, queueId, MSG encMsg)
|
||||
_ -> pure (corrId, queueId, OK)
|
||||
|
||||
withQueue :: (QueueRec -> m (Transmission BrokerMsg)) -> m (Transmission BrokerMsg)
|
||||
withQueue :: (QueueRec -> M (Transmission BrokerMsg)) -> M (Transmission BrokerMsg)
|
||||
withQueue action = maybe (pure $ err AUTH) action qr_
|
||||
|
||||
subscribeNotifications :: m (Transmission BrokerMsg)
|
||||
subscribeNotifications :: M (Transmission BrokerMsg)
|
||||
subscribeNotifications = time "NSUB" . atomically $ do
|
||||
unlessM (TM.member queueId ntfSubscriptions) $ do
|
||||
writeTQueue ntfSubscribedQ (queueId, clnt)
|
||||
TM.insert queueId () ntfSubscriptions
|
||||
pure ok
|
||||
|
||||
acknowledgeMsg :: QueueRec -> MsgId -> m (Transmission BrokerMsg)
|
||||
acknowledgeMsg :: QueueRec -> MsgId -> M (Transmission BrokerMsg)
|
||||
acknowledgeMsg qr msgId = time "ACK" $ do
|
||||
atomically (TM.lookup queueId subscriptions) >>= \case
|
||||
Nothing -> pure $ err NO_MSG
|
||||
@@ -762,19 +841,19 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
if msgId == msgId' || B.null msgId
|
||||
then pure $ Just s
|
||||
else putTMVar delivered msgId' $> Nothing
|
||||
updateStats :: Message -> m ()
|
||||
updateStats :: Message -> M ()
|
||||
updateStats = \case
|
||||
MessageQuota {} -> pure ()
|
||||
Message {msgFlags} -> do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (msgRecv stats) (+ 1)
|
||||
atomically $ modifyTVar' (msgCount stats) (+ 1)
|
||||
atomically $ modifyTVar' (msgCount stats) (subtract 1)
|
||||
atomically $ updatePeriodStats (activeQueues stats) queueId
|
||||
when (notification msgFlags) $ do
|
||||
atomically $ modifyTVar' (msgRecvNtf stats) (+ 1)
|
||||
atomically $ updatePeriodStats (activeQueuesNtf stats) queueId
|
||||
|
||||
sendMessage :: QueueRec -> MsgFlags -> MsgBody -> m (Transmission BrokerMsg)
|
||||
sendMessage :: QueueRec -> MsgFlags -> MsgBody -> M (Transmission BrokerMsg)
|
||||
sendMessage qr msgFlags msgBody
|
||||
| B.length msgBody > maxMessageLength = pure $ err LARGE_MSG
|
||||
| otherwise = case status qr of
|
||||
@@ -796,17 +875,17 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
atomically $ modifyTVar' (msgSentNtf stats) (+ 1)
|
||||
atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr)
|
||||
atomically $ modifyTVar' (msgSent stats) (+ 1)
|
||||
atomically $ modifyTVar' (msgCount stats) (subtract 1)
|
||||
atomically $ modifyTVar' (msgCount stats) (+ 1)
|
||||
atomically $ updatePeriodStats (activeQueues stats) (recipientId qr)
|
||||
pure ok
|
||||
where
|
||||
mkMessage :: C.MaxLenBS MaxMessageLen -> m Message
|
||||
mkMessage :: C.MaxLenBS MaxMessageLen -> M Message
|
||||
mkMessage body = do
|
||||
msgId <- randomId =<< asks (msgIdBytes . config)
|
||||
msgTs <- liftIO getSystemTime
|
||||
pure $ Message msgId msgTs msgFlags body
|
||||
|
||||
expireMessages :: MsgQueue -> m ()
|
||||
expireMessages :: MsgQueue -> M ()
|
||||
expireMessages q = do
|
||||
msgExp <- asks $ messageExpiration . config
|
||||
old <- liftIO $ mapM expireBeforeEpoch msgExp
|
||||
@@ -834,7 +913,59 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
encNMsgMeta = C.cbEncrypt rcvNtfDhSecret cbNonce (smpEncode msgMeta) 128
|
||||
pure . (cbNonce,) $ fromRight "" encNMsgMeta
|
||||
|
||||
deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> m (Transmission BrokerMsg)
|
||||
processForwardedCommand :: EncFwdTransmission -> M BrokerMsg
|
||||
processForwardedCommand (EncFwdTransmission s) = fmap (either id id) . runExceptT $ do
|
||||
-- TODO error
|
||||
THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwError $ ERR INTERNAL) pure thAuth
|
||||
sessSecret <- maybe (throwError $ ERR INTERNAL) pure sessSecret'
|
||||
let proxyNonce = C.cbNonce $ bs corrId
|
||||
-- TODO error
|
||||
s' <- liftEitherWith internalErr $ C.cbDecrypt sessSecret proxyNonce s
|
||||
-- TODO error
|
||||
FwdTransmission {fwdCorrId, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith internalErr $ smpDecode s'
|
||||
-- TODO error - this error is reported to proxy, as we failed to get to client's transmission
|
||||
let clientSecret = C.dh' fwdKey serverPrivKey
|
||||
clientNonce = C.cbNonce $ bs fwdCorrId
|
||||
b <- liftEitherWith internalErr $ C.cbDecrypt clientSecret clientNonce et
|
||||
-- only allowing single forwarded transactions
|
||||
let t' = tDecodeParseValidate thParams' $ L.head $ tParse thParams' b
|
||||
clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret}
|
||||
-- TODO error
|
||||
r <-
|
||||
lift (rejectOrVerify clntThAuth t') >>= \case
|
||||
Left r -> pure r
|
||||
Right t''@(_, (corrId', entId', _)) ->
|
||||
-- Left will not be returned by processCommand, as only SEND command is allowed
|
||||
fromRight (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'')
|
||||
|
||||
-- encode response
|
||||
r' <- case batchTransmissions (batch thParams') (blockSize thParams') [Right (Nothing, encodeTransmission thParams' r)] of
|
||||
[] -> throwE $ ERR INTERNAL -- TODO error
|
||||
TBError _ _ : _ -> throwE $ ERR INTERNAL -- TODO error
|
||||
TBTransmission b' _ : _ -> pure b'
|
||||
TBTransmissions b' _ _ : _ -> pure b'
|
||||
-- encrypt to client
|
||||
r2 <- liftEitherWith internalErr $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength
|
||||
-- encrypt to proxy
|
||||
let fr = FwdResponse {fwdCorrId, fwdResponse = r2}
|
||||
r3 <- liftEitherWith internalErr $ EncFwdResponse <$> C.cbEncrypt sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) paddedForwardedMsgLength
|
||||
pure $ RRES r3
|
||||
where
|
||||
internalErr _ = ERR INTERNAL -- TODO errors
|
||||
THandleParams {thAuth} = thParams'
|
||||
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)
|
||||
-- flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody
|
||||
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
|
||||
s@Sub {subThread = NoSub} ->
|
||||
@@ -845,7 +976,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
_ -> forkSub $> ok
|
||||
_ -> pure ok
|
||||
where
|
||||
forkSub :: m ()
|
||||
forkSub :: M ()
|
||||
forkSub = do
|
||||
atomically . modifyTVar' sub $ \s -> s {subThread = SubPending}
|
||||
t <- mkWeakThreadId =<< forkIO subscriber
|
||||
@@ -863,7 +994,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
void $ setDelivered s msg
|
||||
writeTVar sub $! s {subThread = NoSub}
|
||||
|
||||
time :: T.Text -> m a -> m a
|
||||
time :: T.Text -> M a -> M a
|
||||
time name = timed name queueId
|
||||
|
||||
encryptMsg :: QueueRec -> Message -> RcvMessage
|
||||
@@ -879,13 +1010,13 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
setDelivered :: Sub -> Message -> STM Bool
|
||||
setDelivered s msg = tryPutTMVar (delivered s) (messageId msg)
|
||||
|
||||
getStoreMsgQueue :: T.Text -> RecipientId -> m MsgQueue
|
||||
getStoreMsgQueue :: T.Text -> RecipientId -> M MsgQueue
|
||||
getStoreMsgQueue name rId = time (name <> " getMsgQueue") $ do
|
||||
ms <- asks msgStore
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
atomically $ getMsgQueue ms rId quota
|
||||
|
||||
delQueueAndMsgs :: QueueStore -> m (Transmission BrokerMsg)
|
||||
delQueueAndMsgs :: QueueStore -> M (Transmission BrokerMsg)
|
||||
delQueueAndMsgs st = do
|
||||
withLog (`logDeleteQueue` queueId)
|
||||
ms <- asks msgStore
|
||||
@@ -902,7 +1033,7 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv
|
||||
okResp :: Either ErrorType () -> Transmission BrokerMsg
|
||||
okResp = either err $ const ok
|
||||
|
||||
updateDeletedStats :: (MonadUnliftIO m, MonadReader Env m) => QueueRec -> m ()
|
||||
updateDeletedStats :: QueueRec -> M ()
|
||||
updateDeletedStats q = do
|
||||
stats <- asks serverStats
|
||||
let delSel = if isNothing (senderKey q) then qDeletedNew else qDeletedSecured
|
||||
@@ -910,12 +1041,12 @@ updateDeletedStats q = do
|
||||
atomically $ modifyTVar' (qDeletedAll stats) (+ 1)
|
||||
atomically $ modifyTVar' (qCount stats) (subtract 1)
|
||||
|
||||
withLog :: (MonadUnliftIO m, MonadReader Env m) => (StoreLog 'WriteMode -> IO a) -> m ()
|
||||
withLog :: (StoreLog 'WriteMode -> IO a) -> M ()
|
||||
withLog action = do
|
||||
env <- ask
|
||||
liftIO . mapM_ action $ storeLog (env :: Env)
|
||||
|
||||
timed :: MonadUnliftIO m => T.Text -> RecipientId -> m a -> m a
|
||||
timed :: T.Text -> RecipientId -> M a -> M a
|
||||
timed name qId a = do
|
||||
t <- liftIO getSystemTime
|
||||
r <- a
|
||||
@@ -927,10 +1058,10 @@ timed name qId a = do
|
||||
diff t t' = (systemSeconds t' - systemSeconds t) * sec + fromIntegral (systemNanoseconds t' - systemNanoseconds t)
|
||||
sec = 1000_000000
|
||||
|
||||
randomId :: (MonadUnliftIO m, MonadReader Env m) => Int -> m ByteString
|
||||
randomId :: Int -> M ByteString
|
||||
randomId n = atomically . C.randomBytes n =<< asks random
|
||||
|
||||
saveServerMessages :: (MonadUnliftIO m, MonadReader Env m) => Bool -> m ()
|
||||
saveServerMessages :: Bool -> M ()
|
||||
saveServerMessages keepMsgs = asks (storeMsgsFile . config) >>= mapM_ saveMessages
|
||||
where
|
||||
saveMessages f = do
|
||||
@@ -945,7 +1076,7 @@ saveServerMessages keepMsgs = asks (storeMsgsFile . config) >>= mapM_ saveMessag
|
||||
atomically (getMessages ms rId)
|
||||
>>= mapM_ (B.hPutStrLn h . strEncode . MLRv3 rId)
|
||||
|
||||
restoreServerMessages :: forall m. (MonadUnliftIO m, MonadReader Env m) => m Int
|
||||
restoreServerMessages :: M Int
|
||||
restoreServerMessages = asks (storeMsgsFile . config) >>= \case
|
||||
Just f -> ifM (doesFileExist f) (restoreMessages f) (pure 0)
|
||||
Nothing -> pure 0
|
||||
@@ -955,7 +1086,7 @@ restoreServerMessages = asks (storeMsgsFile . config) >>= \case
|
||||
ms <- asks msgStore
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
old_ <- asks (messageExpiration . config) $>>= (liftIO . fmap Just . expireBeforeEpoch)
|
||||
runExceptT (liftIO (B.readFile f) >>= foldM (\expired -> restoreMsg expired ms quota old_) 0 . B.lines) >>= \case
|
||||
runExceptT (liftIO (LB.readFile f) >>= foldM (\expired -> restoreMsg expired ms quota old_) 0 . LB.lines) >>= \case
|
||||
Left e -> do
|
||||
logError . T.pack $ "error restoring messages: " <> e
|
||||
liftIO exitFailure
|
||||
@@ -964,10 +1095,11 @@ restoreServerMessages = asks (storeMsgsFile . config) >>= \case
|
||||
logInfo "messages restored"
|
||||
pure expired
|
||||
where
|
||||
restoreMsg !expired ms quota old_ s = do
|
||||
restoreMsg !expired ms quota old_ s' = do
|
||||
MLRv3 rId msg <- liftEither . first (msgErr "parsing") $ strDecode s
|
||||
addToMsgQueue rId msg
|
||||
where
|
||||
s = LB.toStrict s'
|
||||
addToMsgQueue rId msg = do
|
||||
(isExpired, logFull) <- atomically $ do
|
||||
q <- getMsgQueue ms rId quota
|
||||
@@ -981,7 +1113,7 @@ restoreServerMessages = asks (storeMsgsFile . config) >>= \case
|
||||
msgErr :: Show e => String -> e -> String
|
||||
msgErr op e = op <> " error (" <> show e <> "): " <> B.unpack (B.take 100 s)
|
||||
|
||||
saveServerStats :: (MonadUnliftIO m, MonadReader Env m) => m ()
|
||||
saveServerStats :: M ()
|
||||
saveServerStats =
|
||||
asks (serverStatsBackupFile . config)
|
||||
>>= mapM_ (\f -> asks serverStats >>= atomically . getServerStatsData >>= liftIO . saveStats f)
|
||||
@@ -991,7 +1123,7 @@ saveServerStats =
|
||||
B.writeFile f $ strEncode stats
|
||||
logInfo "server stats saved"
|
||||
|
||||
restoreServerStats :: (MonadUnliftIO m, MonadReader Env m) => Int -> m ()
|
||||
restoreServerStats :: Int -> M ()
|
||||
restoreServerStats expiredWhileRestoring = asks (serverStatsBackupFile . config) >>= mapM_ restoreStats
|
||||
where
|
||||
restoreStats f = whenM (doesFileExist f) $ do
|
||||
|
||||
@@ -6,9 +6,13 @@ module Simplex.Messaging.Server.Control where
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString (ByteString)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth)
|
||||
|
||||
data CPClientRole = CPRNone | CPRUser | CPRAdmin
|
||||
|
||||
data ControlProtocol
|
||||
= CPSuspend
|
||||
= CPAuth BasicAuth
|
||||
| CPSuspend
|
||||
| CPResume
|
||||
| CPClients
|
||||
| CPStats
|
||||
@@ -24,6 +28,7 @@ data ControlProtocol
|
||||
|
||||
instance StrEncoding ControlProtocol where
|
||||
strEncode = \case
|
||||
CPAuth bs -> "auth " <> strEncode bs
|
||||
CPSuspend -> "suspend"
|
||||
CPResume -> "resume"
|
||||
CPClients -> "clients"
|
||||
@@ -39,6 +44,7 @@ instance StrEncoding ControlProtocol where
|
||||
CPSkip -> ""
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"auth" -> CPAuth <$> (A.space *> strP)
|
||||
"suspend" -> pure CPSuspend
|
||||
"resume" -> pure CPResume
|
||||
"clients" -> pure CPClients
|
||||
|
||||
@@ -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
|
||||
@@ -33,7 +34,7 @@ import Simplex.Messaging.Server.Stats
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport, VersionSMP, VersionRangeSMP)
|
||||
import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP)
|
||||
import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
@@ -53,6 +54,9 @@ data ServerConfig = ServerConfig
|
||||
allowNewQueues :: Bool,
|
||||
-- | simple password that the clients need to pass in handshake to be able to create new queues
|
||||
newQueueBasicAuth :: Maybe BasicAuth,
|
||||
-- | control port passwords,
|
||||
controlPortUserAuth :: Maybe BasicAuth,
|
||||
controlPortAdminAuth :: Maybe BasicAuth,
|
||||
-- | time after which the messages can be removed from the queues and check interval, seconds
|
||||
messageExpiration :: Maybe ExpirationConfig,
|
||||
-- | time after which the socket with inactive client can be disconnected (without any messages or commands, incl. PING),
|
||||
@@ -76,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
|
||||
@@ -107,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
|
||||
@@ -119,8 +126,14 @@ data Server = Server
|
||||
savingLock :: Lock
|
||||
}
|
||||
|
||||
data 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)),
|
||||
@@ -132,7 +145,8 @@ data Client = Client
|
||||
connected :: TVar Bool,
|
||||
createdAt :: SystemTime,
|
||||
rcvActiveAt :: TVar SystemTime,
|
||||
sndActiveAt :: TVar SystemTime
|
||||
sndActiveAt :: TVar SystemTime,
|
||||
proxyClient_ :: TVar (Maybe C.DhSecretX25519) -- this client is actually an SMP proxy
|
||||
}
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub
|
||||
@@ -151,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
|
||||
@@ -163,32 +177,34 @@ newClient nextClientId qSize thVersion sessionId createdAt = do
|
||||
connected <- newTVar True
|
||||
rcvActiveAt <- newTVar createdAt
|
||||
sndActiveAt <- newTVar createdAt
|
||||
return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt}
|
||||
proxyClient_ <- newTVar Nothing
|
||||
return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, proxyClient_}
|
||||
|
||||
newSubscription :: SubscriptionThread -> STM Sub
|
||||
newSubscription subThread = do
|
||||
delivered <- newEmptyTMVar
|
||||
return Sub {subThread, delivered}
|
||||
|
||||
newEnv :: forall m. (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
|
||||
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do
|
||||
newEnv :: ServerConfig -> IO Env
|
||||
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg} = do
|
||||
server <- atomically newServer
|
||||
queueStore <- atomically newQueueStore
|
||||
msgStore <- atomically newMsgStore
|
||||
random <- liftIO C.newRandom
|
||||
storeLog <- restoreQueues queueStore `mapM` storeLogFile
|
||||
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
let serverIdentity = KeyHash fp
|
||||
serverStats <- atomically . newServerStats =<< liftIO getCurrentTime
|
||||
serverStats <- atomically . newServerStats =<< getCurrentTime
|
||||
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 -> m (StoreLog 'WriteMode)
|
||||
restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode)
|
||||
restoreQueues QueueStore {queues, senders, notifiers} f = do
|
||||
(qs, s) <- liftIO $ readWriteStoreLog f
|
||||
(qs, s) <- readWriteStoreLog f
|
||||
atomically $ do
|
||||
writeTVar queues =<< mapM newTVar qs
|
||||
writeTVar senders $! M.foldr' addSender M.empty qs
|
||||
@@ -200,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}
|
||||
|
||||
@@ -18,6 +18,8 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..))
|
||||
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 +27,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, supportedServerSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange, batchCmdsSMPVersion, sendingProxySMPVersion)
|
||||
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)
|
||||
@@ -128,6 +131,8 @@ smpServerCLI cfgPath logPath =
|
||||
_ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')"
|
||||
)
|
||||
<> "\n\n\
|
||||
\# control_port_admin_password:\n\
|
||||
\# control_port_user_password:\n\
|
||||
\[TRANSPORT]\n\
|
||||
\# host is only used to print server address on start\n"
|
||||
<> ("host: " <> host <> "\n")
|
||||
@@ -189,6 +194,8 @@ smpServerCLI cfgPath logPath =
|
||||
-- allow creating new queues by default
|
||||
allowNewQueues = fromMaybe True $ iniOnOff "AUTH" "new_queues" ini,
|
||||
newQueueBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
|
||||
controlPortAdminAuth = either error id <$> strDecodeIni "AUTH" "control_port_admin_password" ini,
|
||||
controlPortUserAuth = either error id <$> strDecodeIni "AUTH" "control_port_user_password" ini,
|
||||
messageExpiration =
|
||||
Just
|
||||
defaultMessageExpiration
|
||||
@@ -209,7 +216,9 @@ smpServerCLI cfgPath logPath =
|
||||
defaultTransportServerConfig
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
|
||||
},
|
||||
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}},
|
||||
allowSMPProxy = True -- TODO: "get from INI"
|
||||
}
|
||||
|
||||
data CliCommand
|
||||
@@ -301,4 +310,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
|
||||
|
||||
@@ -25,8 +25,8 @@ where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Monad (foldM, unless, when)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Functor (($>))
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
@@ -148,13 +148,14 @@ writeQueues s = mapM_ $ \q -> when (active q) $ logCreateQueue s q
|
||||
active QueueRec {status} = status == QueueActive
|
||||
|
||||
readQueues :: FilePath -> IO (Map RecipientId QueueRec)
|
||||
readQueues f = foldM processLine M.empty . B.lines =<< B.readFile f
|
||||
readQueues f = foldM processLine M.empty . LB.lines =<< LB.readFile f
|
||||
where
|
||||
processLine :: Map RecipientId QueueRec -> ByteString -> IO (Map RecipientId QueueRec)
|
||||
processLine m s = case strDecode $ trimCR s of
|
||||
processLine :: Map RecipientId QueueRec -> LB.ByteString -> IO (Map RecipientId QueueRec)
|
||||
processLine m s' = case strDecode $ trimCR s of
|
||||
Right r -> pure $ procLogRecord r
|
||||
Left e -> printError e $> m
|
||||
where
|
||||
s = LB.toStrict s'
|
||||
procLogRecord :: StoreLogRecord -> Map RecipientId QueueRec
|
||||
procLogRecord = \case
|
||||
CreateQueue q -> M.insert (recipientId q) q m
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Session where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Data.Composition ((.:.))
|
||||
import Data.Functor (($>))
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
|
||||
data SessionVar a = SessionVar
|
||||
{ sessionVar :: TMVar a,
|
||||
sessionVarId :: Int
|
||||
}
|
||||
|
||||
getSessVar :: forall k a. Ord k => TVar Int -> k -> TMap k (SessionVar a) -> STM (Either (SessionVar a) (SessionVar a))
|
||||
getSessVar sessSeq sessKey vs = maybe (Left <$> newSessionVar) (pure . Right) =<< TM.lookup sessKey vs
|
||||
where
|
||||
newSessionVar :: STM (SessionVar a)
|
||||
newSessionVar = do
|
||||
sessionVar <- newEmptyTMVar
|
||||
sessionVarId <- stateTVar sessSeq $ \next -> (next, next + 1)
|
||||
let v = SessionVar {sessionVar, sessionVarId}
|
||||
TM.insert sessKey v vs
|
||||
pure v
|
||||
|
||||
removeSessVar :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM ()
|
||||
removeSessVar = void .:. removeSessVar'
|
||||
{-# INLINE removeSessVar #-}
|
||||
|
||||
removeSessVar' :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM Bool
|
||||
removeSessVar' v sessKey vs =
|
||||
TM.lookup sessKey vs >>= \case
|
||||
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True
|
||||
_ -> pure False
|
||||
@@ -12,6 +12,7 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- |
|
||||
@@ -40,6 +41,7 @@ module Simplex.Messaging.Transport
|
||||
basicAuthSMPVersion,
|
||||
subModeSMPVersion,
|
||||
authCmdsSMPVersion,
|
||||
sendingProxySMPVersion,
|
||||
simplexMQVersion,
|
||||
smpBlockSize,
|
||||
TransportConfig (..),
|
||||
@@ -54,6 +56,7 @@ module Simplex.Messaging.Transport
|
||||
-- * TLS Transport
|
||||
TLS (..),
|
||||
SessionId,
|
||||
ALPN,
|
||||
connectTLS,
|
||||
closeTLS,
|
||||
supportedParameters,
|
||||
@@ -76,7 +79,7 @@ module Simplex.Messaging.Transport
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Monad (forM)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except (throwE)
|
||||
@@ -112,6 +115,11 @@ import UnliftIO.STM
|
||||
|
||||
-- * Transport parameters
|
||||
|
||||
-- min size it works with:
|
||||
-- unsigned message: 16292 (paddedProxiedMsgLength = 16151, paddedForwardedMsgLength = 16239)
|
||||
-- Ed448: 16406 (16384 + 22, fails with 21)
|
||||
-- Ed25519: 16356
|
||||
-- X25519: 16381
|
||||
smpBlockSize :: Int
|
||||
smpBlockSize = 16384
|
||||
|
||||
@@ -147,6 +155,9 @@ subModeSMPVersion = VersionSMP 6
|
||||
authCmdsSMPVersion :: VersionSMP
|
||||
authCmdsSMPVersion = VersionSMP 7
|
||||
|
||||
sendingProxySMPVersion :: VersionSMP
|
||||
sendingProxySMPVersion = VersionSMP 8
|
||||
|
||||
currentClientSMPRelayVersion :: VersionSMP
|
||||
currentClientSMPRelayVersion = VersionSMP 6
|
||||
|
||||
@@ -228,10 +239,13 @@ data TLS = TLS
|
||||
tlsPeer :: TransportPeer,
|
||||
tlsUniq :: ByteString,
|
||||
tlsBuffer :: TBuffer,
|
||||
tlsALPN :: Maybe ALPN,
|
||||
tlsServerCerts :: X.CertificateChain,
|
||||
tlsTransportConfig :: TransportConfig
|
||||
}
|
||||
|
||||
type ALPN = ByteString
|
||||
|
||||
connectTLS :: T.TLSParams p => Maybe HostName -> TransportConfig -> p -> Socket -> IO T.Context
|
||||
connectTLS host_ TransportConfig {logTLSErrors} params sock =
|
||||
E.bracketOnError (T.contextNew sock params) closeTLS $ \ctx ->
|
||||
@@ -246,7 +260,8 @@ getTLS tlsPeer cfg tlsServerCerts cxt = withTlsUnique tlsPeer cxt newTLS
|
||||
where
|
||||
newTLS tlsUniq = do
|
||||
tlsBuffer <- atomically newTBuffer
|
||||
pure TLS {tlsContext = cxt, tlsTransportConfig = cfg, tlsServerCerts, tlsPeer, tlsUniq, tlsBuffer}
|
||||
tlsALPN <- T.getNegotiatedProtocol cxt
|
||||
pure TLS {tlsContext = cxt, tlsALPN, tlsTransportConfig = cfg, tlsServerCerts, tlsPeer, tlsUniq, tlsBuffer}
|
||||
|
||||
withTlsUnique :: TransportPeer -> T.Context -> (ByteString -> IO c) -> IO c
|
||||
withTlsUnique peer cxt f =
|
||||
@@ -306,20 +321,20 @@ instance Transport TLS where
|
||||
-- * SMP transport
|
||||
|
||||
-- | The handle for SMP encrypted transport connection over Transport.
|
||||
data THandle v c = THandle
|
||||
data THandle v c p = THandle
|
||||
{ connection :: c,
|
||||
params :: THandleParams v
|
||||
params :: THandleParams v p
|
||||
}
|
||||
|
||||
type THandleSMP c = THandle SMPVersion c
|
||||
type THandleSMP c p = THandle SMPVersion c p
|
||||
|
||||
data THandleParams v = THandleParams
|
||||
data THandleParams v p = THandleParams
|
||||
{ sessionId :: SessionId,
|
||||
blockSize :: Int,
|
||||
-- | agreed server protocol version
|
||||
thVersion :: Version v,
|
||||
-- | peer public key for command authorization and shared secrets for entity ID encryption
|
||||
thAuth :: Maybe THandleAuth,
|
||||
thAuth :: Maybe (THandleAuth p),
|
||||
-- | do NOT send session ID in transmission, but include it into signed message
|
||||
-- based on protocol version
|
||||
implySessId :: Bool,
|
||||
@@ -328,10 +343,18 @@ data THandleParams v = THandleParams
|
||||
batch :: Bool
|
||||
}
|
||||
|
||||
data THandleAuth = THandleAuth
|
||||
{ peerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key
|
||||
privKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server)
|
||||
}
|
||||
data THandleAuth (p :: TransportPeer) where
|
||||
THAuthClient ::
|
||||
{ serverPeerPubKey :: C.PublicKeyX25519, -- used by the client to combine with client's private per-queue key
|
||||
serverCertKey :: (X.CertificateChain, X.SignedExact X.PubKey), -- the key here is serverPeerPubKey signed with server certificate
|
||||
sessSecret :: Maybe C.DhSecretX25519 -- session secret (will be used in SMP proxy only)
|
||||
} ->
|
||||
THandleAuth 'TClient
|
||||
THAuthServer ::
|
||||
{ serverPrivKey :: C.PrivateKeyX25519, -- used by the server to combine with client's public per-queue key
|
||||
sessSecret' :: Maybe C.DhSecretX25519 -- session secret (will be used in SMP proxy only)
|
||||
} ->
|
||||
THandleAuth 'TServer
|
||||
|
||||
-- | TLS-unique channel binding
|
||||
type SessionId = ByteString
|
||||
@@ -340,6 +363,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)
|
||||
}
|
||||
|
||||
@@ -385,7 +409,7 @@ encodeAuthEncryptCmds v k
|
||||
| otherwise = ""
|
||||
|
||||
authEncryptCmdsP :: VersionSMP -> Parser a -> Parser (Maybe a)
|
||||
authEncryptCmdsP v p = if v >= authCmdsSMPVersion then Just <$> p else pure Nothing
|
||||
authEncryptCmdsP v p = if v >= authCmdsSMPVersion then optional p else pure Nothing
|
||||
|
||||
-- | Error of SMP encrypted transport over TCP.
|
||||
data TransportError
|
||||
@@ -433,13 +457,13 @@ serializeTransportError = \case
|
||||
TEHandshake e -> "HANDSHAKE " <> bshow e
|
||||
|
||||
-- | Pad and send block to SMP transport.
|
||||
tPutBlock :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ())
|
||||
tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ())
|
||||
tPutBlock THandle {connection = c, params = THandleParams {blockSize}} block =
|
||||
bimapM (const $ pure TELargeMsg) (cPut c) $
|
||||
C.pad block blockSize
|
||||
|
||||
-- | Receive block from SMP transport.
|
||||
tGetBlock :: Transport c => THandle v c -> IO (Either TransportError ByteString)
|
||||
tGetBlock :: Transport c => THandle v c p -> IO (Either TransportError ByteString)
|
||||
tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do
|
||||
msg <- cGet c blockSize
|
||||
if B.length msg == blockSize
|
||||
@@ -449,7 +473,7 @@ tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do
|
||||
-- | Server SMP transport handshake.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
|
||||
smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c)
|
||||
smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TServer)
|
||||
smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do
|
||||
let th@THandle {params = THandleParams {sessionId}} = smpTHandle c
|
||||
sk = C.signX509 serverSignKey $ C.publicToX509 k
|
||||
@@ -460,47 +484,56 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do
|
||||
| keyHash /= kh ->
|
||||
throwE $ TEHandshake IDENTITY
|
||||
| v `isCompatible` smpVRange ->
|
||||
pure $ smpThHandle th v pk k'
|
||||
pure $ smpThHandleServer th v pk k'
|
||||
| otherwise -> throwE $ TEHandshake VERSION
|
||||
|
||||
-- | Client SMP transport handshake.
|
||||
--
|
||||
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
|
||||
smpClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c)
|
||||
smpClientHandshake c (k, pk) keyHash@(C.KeyHash kh) smpVRange = do
|
||||
smpClientHandshake :: forall c. Transport c => c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TClient)
|
||||
smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do
|
||||
let th@THandle {params = THandleParams {sessionId}} = smpTHandle c
|
||||
ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th
|
||||
if sessionId /= sessId
|
||||
then throwE TEBadSession
|
||||
else case smpVersionRange `compatibleVersion` smpVRange of
|
||||
Just (Compatible v) -> do
|
||||
sk_ <- forM authPubKey $ \(X.CertificateChain cert, exact) ->
|
||||
ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) ->
|
||||
liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
|
||||
case cert of
|
||||
[_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure ()
|
||||
_ -> throwError "bad certificate"
|
||||
serverKey <- getServerVerifyKey c
|
||||
pubKey <- C.verifyX509 serverKey exact
|
||||
C.x509ToPublic (pubKey, []) >>= C.pubKey
|
||||
sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = Just k}
|
||||
pure $ smpThHandle th v pk sk_
|
||||
(,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
|
||||
sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_}
|
||||
pure $ smpThHandleClient th v (snd <$> ks_) ck_
|
||||
Nothing -> throwE $ TEHandshake VERSION
|
||||
|
||||
smpThHandle :: forall c. THandleSMP c -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c
|
||||
smpThHandle th@THandle {params} v privKey k_ =
|
||||
-- TODO drop SMP v6: make thAuth non-optional
|
||||
let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_
|
||||
params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion}
|
||||
in (th :: THandleSMP c) {params = params'}
|
||||
smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer
|
||||
smpThHandleServer th v pk k_ =
|
||||
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_}
|
||||
in smpThHandle_ th v (Just thAuth)
|
||||
|
||||
sendHandshake :: (Transport c, Encoding smp) => THandle v c -> smp -> ExceptT TransportError IO ()
|
||||
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_ =
|
||||
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_
|
||||
in smpThHandle_ th v thAuth
|
||||
|
||||
smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p
|
||||
smpThHandle_ th@THandle {params} v thAuth =
|
||||
-- TODO drop SMP v6: make thAuth non-optional
|
||||
let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion}
|
||||
in (th :: THandleSMP c p) {params = params'}
|
||||
|
||||
sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO ()
|
||||
sendHandshake th = ExceptT . tPutBlock th . smpEncode
|
||||
|
||||
-- ignores tail bytes to allow future extensions
|
||||
getHandshake :: (Transport c, Encoding smp) => THandle v c -> ExceptT TransportError IO smp
|
||||
getHandshake :: (Transport c, Encoding smp) => THandle v c p -> ExceptT TransportError IO smp
|
||||
getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP =<<) <$> tGetBlock th
|
||||
|
||||
smpTHandle :: Transport c => c -> THandleSMP c
|
||||
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}
|
||||
|
||||
@@ -17,13 +17,13 @@ module Simplex.Messaging.Transport.Client
|
||||
TransportHost (..),
|
||||
TransportHosts (..),
|
||||
TransportHosts_ (..),
|
||||
validateCertificateChain
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -114,30 +114,31 @@ data TransportClientConfig = TransportClientConfig
|
||||
{ socksProxy :: Maybe SocksProxy,
|
||||
tcpKeepAlive :: Maybe KeepAliveOpts,
|
||||
logTLSErrors :: Bool,
|
||||
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey)
|
||||
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey),
|
||||
alpn :: Maybe [ALPN]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultTransportClientConfig :: TransportClientConfig
|
||||
defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True Nothing
|
||||
defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True Nothing Nothing
|
||||
|
||||
clientTransportConfig :: TransportClientConfig -> TransportConfig
|
||||
clientTransportConfig TransportClientConfig {logTLSErrors} =
|
||||
TransportConfig {logTLSErrors, transportTimeout = Nothing}
|
||||
|
||||
-- | Connect to passed TCP host:port and pass handle to the client.
|
||||
runTransportClient :: (Transport c, MonadUnliftIO m) => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> m a) -> m a
|
||||
runTransportClient :: Transport c => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
|
||||
runTransportClient = runTLSTransportClient supportedParameters Nothing
|
||||
|
||||
runTLSTransportClient :: (Transport c, MonadUnliftIO m) => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> m a) -> m a
|
||||
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials} proxyUsername host port keyHash client = do
|
||||
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
|
||||
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} proxyUsername host port keyHash client = do
|
||||
serverCert <- newEmptyTMVarIO
|
||||
let hostName = B.unpack $ strEncode host
|
||||
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials serverCert
|
||||
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert
|
||||
connectTCP = case socksProxy of
|
||||
Just proxy -> connectSocksClient proxy proxyUsername $ hostAddr host
|
||||
_ -> connectTCPClient hostName
|
||||
c <- liftIO $ do
|
||||
c <- do
|
||||
sock <- connectTCP port
|
||||
mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e)
|
||||
let tCfg = clientTransportConfig cfg
|
||||
@@ -148,7 +149,7 @@ runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy,
|
||||
closeTLS tls >> error "onServerCertificate failed"
|
||||
Just c -> pure c
|
||||
getClientConnection tCfg chain tls
|
||||
client c `E.finally` liftIO (closeConnection c)
|
||||
client c `E.finally` closeConnection c
|
||||
where
|
||||
hostAddr = \case
|
||||
THIPv4 addr -> SocksAddrIPV4 $ tupleToHostAddress addr
|
||||
@@ -216,14 +217,15 @@ instance ToJSON SocksProxy where
|
||||
instance FromJSON SocksProxy where
|
||||
parseJSON = strParseJSON "SocksProxy"
|
||||
|
||||
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> TMVar X.CertificateChain -> T.ClientParams
|
||||
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ serverCerts =
|
||||
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> TMVar X.CertificateChain -> T.ClientParams
|
||||
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ serverCerts =
|
||||
(T.defaultParamsClient host p)
|
||||
{ T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
|
||||
T.clientHooks =
|
||||
def
|
||||
{ T.onServerCertificate = onServerCert,
|
||||
T.onCertificateRequest = maybe def (const . pure . Just) clientCreds_
|
||||
T.onCertificateRequest = maybe def (const . pure . Just) clientCreds_,
|
||||
T.onSuggestALPN = pure alpn_
|
||||
},
|
||||
T.clientSupported = supported
|
||||
}
|
||||
@@ -238,7 +240,7 @@ mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ serverCerts =
|
||||
validateCertificateChain :: C.KeyHash -> HostName -> ByteString -> X.CertificateChain -> IO [XV.FailedReason]
|
||||
validateCertificateChain _ _ _ (X.CertificateChain []) = pure [XV.EmptyChain]
|
||||
validateCertificateChain _ _ _ (X.CertificateChain [_]) = pure [XV.EmptyChain]
|
||||
validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_, caCert]) =
|
||||
validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain [_, caCert]) =
|
||||
if Fingerprint kh == XV.getFingerprint caCert X.HashSHA256
|
||||
then x509validate
|
||||
else pure [XV.UnknownCA]
|
||||
@@ -248,7 +250,7 @@ validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_,
|
||||
where
|
||||
hooks = XV.defaultHooks
|
||||
checks = XV.defaultChecks {XV.checkFQHN = False}
|
||||
certStore = XS.makeCertificateStore sc
|
||||
certStore = XS.makeCertificateStore [caCert]
|
||||
cache = XV.exceptionValidationCache [] -- we manually check fingerprint only of the identity certificate (ca.crt)
|
||||
serviceID = (host, port)
|
||||
validateCertificateChain _ _ _ _ = pure [XV.AuthorityTooDeep]
|
||||
|
||||
@@ -16,15 +16,15 @@ import qualified Network.HTTP2.Server as HS
|
||||
import Network.Socket (SockAddr (..))
|
||||
import qualified Network.TLS as T
|
||||
import qualified Network.TLS.Extra as TE
|
||||
import Simplex.Messaging.Transport (SessionId, TLS (tlsUniq), Transport (cGet, cPut))
|
||||
import Simplex.Messaging.Transport (TLS, Transport (cGet, cPut))
|
||||
import Simplex.Messaging.Transport.Buffer
|
||||
import qualified System.TimeManager as TI
|
||||
|
||||
defaultHTTP2BufferSize :: BufferSize
|
||||
defaultHTTP2BufferSize = 32768
|
||||
|
||||
withHTTP2 :: BufferSize -> (Config -> SessionId -> IO a) -> TLS -> IO a
|
||||
withHTTP2 sz run c = E.bracket (allocHTTP2Config c sz) freeSimpleConfig (`run` tlsUniq c)
|
||||
withHTTP2 :: BufferSize -> (Config -> IO a) -> IO () -> TLS -> IO a
|
||||
withHTTP2 sz run fin c = E.bracket (allocHTTP2Config c sz) (\cfg -> freeSimpleConfig cfg `E.finally` fin) run
|
||||
|
||||
allocHTTP2Config :: TLS -> BufferSize -> IO Config
|
||||
allocHTTP2Config c sz = do
|
||||
|
||||
@@ -23,15 +23,20 @@ import qualified Network.TLS as T
|
||||
import Numeric.Natural (Natural)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Transport (SessionId, TLS)
|
||||
import Simplex.Messaging.Transport (ALPN, SessionId, TLS (tlsALPN), getServerCerts, getServerVerifyKey, tlsUniq)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), runTLSTransportClient)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Util (eitherToMaybe)
|
||||
import UnliftIO.STM
|
||||
import UnliftIO.Timeout
|
||||
import qualified Data.X509 as X
|
||||
|
||||
data HTTP2Client = HTTP2Client
|
||||
{ action :: Maybe (Async HTTP2Response),
|
||||
sessionId :: SessionId,
|
||||
sessionALPN :: Maybe ALPN,
|
||||
serverKey :: Maybe C.APublicVerifyKey, -- may not always be a key we control (i.e. APNS with apple-mandated key types)
|
||||
serverCerts :: X.CertificateChain,
|
||||
sessionTs :: UTCTime,
|
||||
sendReq :: Request -> (Response -> IO HTTP2Response) -> IO HTTP2Response,
|
||||
client_ :: HClient
|
||||
@@ -66,7 +71,7 @@ defaultHTTP2ClientConfig =
|
||||
HTTP2ClientConfig
|
||||
{ qSize = 64,
|
||||
connTimeout = 10000000,
|
||||
transportConfig = TransportClientConfig Nothing Nothing True Nothing,
|
||||
transportConfig = TransportClientConfig Nothing Nothing True Nothing Nothing,
|
||||
bufferSize = defaultHTTP2BufferSize,
|
||||
bodyHeadSize = 16384,
|
||||
suportedTLSParams = http2TLSParams
|
||||
@@ -86,9 +91,10 @@ getVerifiedHTTP2Client proxyUsername host port keyHash caStore config disconnect
|
||||
attachHTTP2Client :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> Int -> TLS -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
attachHTTP2Client config host port disconnected bufferSize tls = getVerifiedHTTP2ClientWith config host port disconnected setup
|
||||
where
|
||||
setup :: (TLS -> H.Client HTTP2Response) -> IO HTTP2Response
|
||||
setup = runHTTP2ClientWith bufferSize host ($ tls)
|
||||
|
||||
getVerifiedHTTP2ClientWith :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> ((SessionId -> H.Client HTTP2Response) -> IO HTTP2Response) -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getVerifiedHTTP2ClientWith :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> ((TLS -> H.Client HTTP2Response) -> IO HTTP2Response) -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getVerifiedHTTP2ClientWith config host port disconnected setup =
|
||||
(atomically mkHTTPS2Client >>= runClient)
|
||||
`E.catch` \(e :: IOException) -> pure . Left $ HCIOError e
|
||||
@@ -104,15 +110,25 @@ getVerifiedHTTP2ClientWith config host port disconnected setup =
|
||||
cVar <- newEmptyTMVarIO
|
||||
action <- async $ setup (client c cVar) `E.finally` atomically (putTMVar cVar $ Left HCNetworkError)
|
||||
c_ <- connTimeout config `timeout` atomically (takeTMVar cVar)
|
||||
pure $ case c_ of
|
||||
Just (Right c') -> Right c' {action = Just action}
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left HCNetworkError
|
||||
case c_ of
|
||||
Just (Right c') -> pure $ Right c' {action = Just action}
|
||||
Just (Left e) -> pure $ Left e
|
||||
Nothing -> cancel action $> Left HCNetworkError
|
||||
|
||||
client :: HClient -> TMVar (Either HTTP2ClientError HTTP2Client) -> SessionId -> H.Client HTTP2Response
|
||||
client c cVar sessionId sendReq = do
|
||||
client :: HClient -> TMVar (Either HTTP2ClientError HTTP2Client) -> TLS -> H.Client HTTP2Response
|
||||
client c cVar tls sendReq = do
|
||||
sessionTs <- getCurrentTime
|
||||
let c' = HTTP2Client {action = Nothing, client_ = c, sendReq, sessionId, sessionTs}
|
||||
let c' =
|
||||
HTTP2Client
|
||||
{ action = Nothing,
|
||||
client_ = c,
|
||||
serverKey = eitherToMaybe $ getServerVerifyKey tls,
|
||||
serverCerts = getServerCerts tls,
|
||||
sendReq,
|
||||
sessionTs,
|
||||
sessionId = tlsUniq tls,
|
||||
sessionALPN = tlsALPN tls
|
||||
}
|
||||
atomically $ do
|
||||
writeTVar (connected c) True
|
||||
putTMVar cVar (Right c')
|
||||
@@ -154,13 +170,14 @@ sendRequestDirect HTTP2Client {client_ = HClient {config, disconnected}, sendReq
|
||||
http2RequestTimeout :: HTTP2ClientConfig -> Maybe Int -> Int
|
||||
http2RequestTimeout HTTP2ClientConfig {connTimeout} = maybe connTimeout (connTimeout +)
|
||||
|
||||
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (SessionId -> H.Client a) -> IO a
|
||||
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (TLS -> H.Client a) -> IO a
|
||||
runHTTP2Client tlsParams caStore tcConfig bufferSize proxyUsername host port keyHash = runHTTP2ClientWith bufferSize host setup
|
||||
where
|
||||
setup :: (TLS -> IO a) -> IO a
|
||||
setup = runTLSTransportClient tlsParams caStore tcConfig proxyUsername host port keyHash
|
||||
|
||||
runHTTP2ClientWith :: forall a. BufferSize -> TransportHost -> ((TLS -> IO a) -> IO a) -> (SessionId -> H.Client a) -> IO a
|
||||
runHTTP2ClientWith bufferSize host setup client = setup $ withHTTP2 bufferSize run
|
||||
runHTTP2ClientWith :: forall a. BufferSize -> TransportHost -> ((TLS -> IO a) -> IO a) -> (TLS -> H.Client a) -> IO a
|
||||
runHTTP2ClientWith bufferSize host setup client = setup $ \tls -> withHTTP2 bufferSize (run tls) (pure ()) tls
|
||||
where
|
||||
run :: H.Config -> SessionId -> IO a
|
||||
run cfg sessId = H.run (ClientConfig "https" (strEncode host) 20) cfg $ client sessId
|
||||
run :: TLS -> H.Config -> IO a
|
||||
run tls cfg = H.run (ClientConfig "https" (strEncode host) 20) cfg $ client tls
|
||||
|
||||
@@ -13,14 +13,14 @@ import Network.Socket
|
||||
import qualified Network.TLS as T
|
||||
import Numeric.Natural (Natural)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (SessionId, TLS, closeConnection)
|
||||
import Simplex.Messaging.Transport (ALPN, SessionId, TLS, closeConnection, tlsALPN, tlsUniq)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadSupportedTLSServerParams, runTransportServer)
|
||||
import Simplex.Messaging.Util (threadDelay')
|
||||
import UnliftIO (finally)
|
||||
import UnliftIO.Concurrent (forkIO, killThread)
|
||||
|
||||
type HTTP2ServerFunc = SessionId -> Request -> (Response -> IO ()) -> IO ()
|
||||
type HTTP2ServerFunc = SessionId -> Maybe ALPN -> Request -> (Response -> IO ()) -> IO ()
|
||||
|
||||
data HTTP2ServerConfig = HTTP2ServerConfig
|
||||
{ qSize :: Natural,
|
||||
@@ -37,6 +37,7 @@ data HTTP2ServerConfig = HTTP2ServerConfig
|
||||
|
||||
data HTTP2Request = HTTP2Request
|
||||
{ sessionId :: SessionId,
|
||||
sessionALPN :: Maybe ALPN,
|
||||
request :: Request,
|
||||
reqBody :: HTTP2Body,
|
||||
sendResponse :: Response -> IO ()
|
||||
@@ -54,32 +55,32 @@ getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, se
|
||||
started <- newEmptyTMVarIO
|
||||
reqQ <- newTBQueueIO qSize
|
||||
action <- async $
|
||||
runHTTP2Server started http2Port bufferSize tlsServerParams transportConfig Nothing $ \sessionId r sendResponse -> do
|
||||
runHTTP2Server started http2Port bufferSize tlsServerParams transportConfig Nothing (const $ pure ()) $ \sessionId sessionALPN r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r bodyHeadSize
|
||||
atomically $ writeTBQueue reqQ HTTP2Request {sessionId, request = r, reqBody, sendResponse}
|
||||
atomically $ writeTBQueue reqQ HTTP2Request {sessionId, sessionALPN, request = r, reqBody, sendResponse}
|
||||
void . atomically $ takeTMVar started
|
||||
pure HTTP2Server {action, reqQ}
|
||||
|
||||
closeHTTP2Server :: HTTP2Server -> IO ()
|
||||
closeHTTP2Server = uninterruptibleCancel . action
|
||||
|
||||
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.ServerParams -> TransportServerConfig -> Maybe ExpirationConfig -> HTTP2ServerFunc -> IO ()
|
||||
runHTTP2Server started port bufferSize serverParams transportConfig expCfg_ = runHTTP2ServerWith_ expCfg_ bufferSize setup
|
||||
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.ServerParams -> TransportServerConfig -> Maybe ExpirationConfig -> (SessionId -> IO ()) -> HTTP2ServerFunc -> IO ()
|
||||
runHTTP2Server started port bufferSize serverParams transportConfig expCfg_ clientFinished = runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup
|
||||
where
|
||||
setup = runTransportServer started port serverParams transportConfig
|
||||
|
||||
runHTTP2ServerWith :: BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
|
||||
runHTTP2ServerWith = runHTTP2ServerWith_ Nothing
|
||||
runHTTP2ServerWith = runHTTP2ServerWith_ Nothing (\_sessId -> pure ())
|
||||
|
||||
runHTTP2ServerWith_ :: Maybe ExpirationConfig -> BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
|
||||
runHTTP2ServerWith_ expCfg_ bufferSize setup http2Server = setup $ \tls -> do
|
||||
runHTTP2ServerWith_ :: Maybe ExpirationConfig -> (SessionId -> IO ()) -> BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
|
||||
runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup http2Server = setup $ \tls -> do
|
||||
activeAt <- newTVarIO =<< getSystemTime
|
||||
tid_ <- mapM (forkIO . expireInactiveClient tls activeAt) expCfg_
|
||||
withHTTP2 bufferSize (run activeAt) tls `finally` mapM_ killThread tid_
|
||||
withHTTP2 bufferSize (run tls activeAt) (clientFinished $ tlsUniq tls) tls `finally` mapM_ killThread tid_
|
||||
where
|
||||
run activeAt cfg sessId = H.run cfg $ \req _aux sendResp -> do
|
||||
run tls activeAt cfg = H.run cfg $ \req _aux sendResp -> do
|
||||
getSystemTime >>= atomically . writeTVar activeAt
|
||||
http2Server sessId req (`sendResp` [])
|
||||
http2Server (tlsUniq tls) (tlsALPN tls) req (`sendResp` [])
|
||||
expireInactiveClient tls activeAt expCfg = loop
|
||||
where
|
||||
loop = do
|
||||
|
||||
@@ -26,7 +26,6 @@ where
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import qualified Crypto.Store.X509 as SX
|
||||
import Data.Default (def)
|
||||
import Data.List (find)
|
||||
@@ -50,7 +49,8 @@ import UnliftIO.STM
|
||||
data TransportServerConfig = TransportServerConfig
|
||||
{ logTLSErrors :: Bool,
|
||||
tlsSetupTimeout :: Int,
|
||||
transportTimeout :: Int
|
||||
transportTimeout :: Int,
|
||||
alpn :: Maybe [ALPN]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -59,7 +59,8 @@ defaultTransportServerConfig =
|
||||
TransportServerConfig
|
||||
{ logTLSErrors = True,
|
||||
tlsSetupTimeout = 60000000,
|
||||
transportTimeout = 40000000
|
||||
transportTimeout = 40000000,
|
||||
alpn = Nothing
|
||||
}
|
||||
|
||||
serverTransportConfig :: TransportServerConfig -> TransportConfig
|
||||
@@ -70,27 +71,26 @@ serverTransportConfig TransportServerConfig {logTLSErrors} =
|
||||
-- | Run transport server (plain TCP or WebSockets) on passed TCP port and signal when server started and stopped via passed TMVar.
|
||||
--
|
||||
-- All accepted connections are passed to the passed function.
|
||||
runTransportServer :: forall c m. (Transport c, MonadUnliftIO m) => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> m ()) -> m ()
|
||||
runTransportServer :: forall c. Transport c => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServer started port params cfg server = do
|
||||
ss <- atomically newSocketState
|
||||
runTransportServerState ss started port params cfg server
|
||||
|
||||
runTransportServerState :: forall c m. (Transport c, MonadUnliftIO m) => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> m ()) -> m ()
|
||||
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started port) (transportName (TProxy :: TProxy c))
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
runTransportServerSocket :: (MonadUnliftIO m, Transport a) => TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> m ()) -> m ()
|
||||
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
|
||||
runTransportServerSocket started getSocket threadLabel serverParams cfg server = do
|
||||
ss <- atomically newSocketState
|
||||
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
runTransportServerSocketState :: (MonadUnliftIO m, Transport a) => SocketState -> TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> m ()) -> m ()
|
||||
runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
|
||||
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server = do
|
||||
u <- askUnliftIO
|
||||
labelMyThread $ "transport server for " <> threadLabel
|
||||
liftIO . runTCPServerSocket ss started getSocket $ \conn ->
|
||||
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection (unliftIO u . server)
|
||||
runTCPServerSocket ss started getSocket $ \conn ->
|
||||
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection server
|
||||
where
|
||||
tCfg = serverTransportConfig cfg
|
||||
setup conn = timeout (tlsSetupTimeout cfg) $ do
|
||||
@@ -116,7 +116,7 @@ runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket serve
|
||||
forever . E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do
|
||||
cId <- atomically $ stateTVar accepted $ \cId -> let cId' = cId + 1 in cId `seq` (cId', cId')
|
||||
let closeConn _ = do
|
||||
atomically $ modifyTVar' clients $ IM.delete cId
|
||||
atomically $ modifyTVar' clients $ IM.delete cId
|
||||
gracefulClose conn 5000 `catchAll_` pure () -- catchAll_ is needed here in case the connection was closed earlier
|
||||
atomically $ modifyTVar' gracefullyClosed (+1)
|
||||
tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn
|
||||
|
||||
@@ -50,26 +50,18 @@ maybeWord :: (a -> ByteString) -> Maybe a -> ByteString
|
||||
maybeWord f = maybe "" $ B.cons ' ' . f
|
||||
{-# INLINE maybeWord #-}
|
||||
|
||||
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither a = liftIO a >>= liftEither
|
||||
{-# INLINE liftIOEither #-}
|
||||
|
||||
liftError :: (MonadIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
|
||||
liftError f = liftEitherError f . runExceptT
|
||||
liftError :: MonadIO m => (e -> e') -> ExceptT e IO a -> ExceptT e' m a
|
||||
liftError f = liftError' f . runExceptT
|
||||
{-# INLINE liftError #-}
|
||||
|
||||
liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
|
||||
liftEitherError f a = liftIOEither (first f <$> a)
|
||||
{-# INLINE liftEitherError #-}
|
||||
liftError' :: MonadIO m => (e -> e') -> IO (Either e a) -> ExceptT e' m a
|
||||
liftError' f = ExceptT . fmap (first f) . liftIO
|
||||
{-# INLINE liftError' #-}
|
||||
|
||||
liftEitherWith :: MonadError e' m => (e -> e') -> Either e a -> m a
|
||||
liftEitherWith :: MonadIO m => (e -> e') -> Either e a -> ExceptT e' m a
|
||||
liftEitherWith f = liftEither . first f
|
||||
{-# INLINE liftEitherWith #-}
|
||||
|
||||
liftE :: (e -> e') -> ExceptT e IO a -> ExceptT e' IO a
|
||||
liftE f a = ExceptT $ first f <$> runExceptT a
|
||||
{-# INLINE liftE #-}
|
||||
|
||||
ifM :: Monad m => m Bool -> m a -> m a -> m a
|
||||
ifM ba t f = ba >>= \b -> if b then t else f
|
||||
{-# INLINE ifM #-}
|
||||
@@ -109,10 +101,18 @@ tryAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m
|
||||
tryAllErrors err action = tryError action `UE.catch` (pure . Left . err)
|
||||
{-# INLINE tryAllErrors #-}
|
||||
|
||||
tryAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> m (Either e a)
|
||||
tryAllErrors' err action = runExceptT action `UE.catch` (pure . Left . err)
|
||||
{-# INLINE tryAllErrors' #-}
|
||||
|
||||
catchAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> (e -> m a) -> m a
|
||||
catchAllErrors err action handler = tryAllErrors err action >>= either handler pure
|
||||
{-# INLINE catchAllErrors #-}
|
||||
|
||||
catchAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> (e -> m a) -> m a
|
||||
catchAllErrors' err action handler = tryAllErrors' err action >>= either handler pure
|
||||
{-# INLINE catchAllErrors' #-}
|
||||
|
||||
catchThrow :: (MonadUnliftIO m, MonadError e m) => m a -> (E.SomeException -> e) -> m a
|
||||
catchThrow action err = catchAllErrors err action throwError
|
||||
{-# INLINE catchThrow #-}
|
||||
@@ -148,8 +148,8 @@ safeDecodeUtf8 = decodeUtf8With onError
|
||||
where
|
||||
onError _ _ = Just '?'
|
||||
|
||||
timeoutThrow :: (MonadUnliftIO m, MonadError e m) => e -> Int -> m a -> m a
|
||||
timeoutThrow e ms action = timeout ms action >>= maybe (throwError e) pure
|
||||
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
|
||||
|
||||
@@ -103,14 +103,14 @@ connectRCHost drg pairing@RCHostPairing {caKey, caCert, idPrivKey, knownHost} ct
|
||||
found@(RCCtrlAddress {address} :| _) <- findCtrlAddress
|
||||
c@RCHClient_ {startedPort, announcer} <- liftIO mkClient
|
||||
hostKeys <- atomically genHostKeys
|
||||
action <- runClient c r hostKeys `putRCError` r
|
||||
action <- liftIO $ runClient c r hostKeys
|
||||
-- wait for the port to make invitation
|
||||
portNum <- atomically $ readTMVar startedPort
|
||||
signedInv@RCSignedInvitation {invitation} <- maybe (throwError RCETLSStartFailed) (liftIO . mkInvitation hostKeys address) portNum
|
||||
when multicast $ case knownHost of
|
||||
Nothing -> throwError RCENewController
|
||||
Just KnownHostPairing {hostDhPubKey} -> do
|
||||
ann <- async . liftIO . runExceptT $ announceRC drg 60 idPrivKey hostDhPubKey hostKeys invitation
|
||||
ann <- liftIO . async . runExceptT $ announceRC drg 60 idPrivKey hostDhPubKey hostKeys invitation
|
||||
atomically $ putTMVar announcer ann
|
||||
pure (found, signedInv, RCHostClient {action, client_ = c}, r)
|
||||
where
|
||||
@@ -125,9 +125,9 @@ connectRCHost drg pairing@RCHostPairing {caKey, caCert, idPrivKey, knownHost} ct
|
||||
endSession <- newEmptyTMVarIO
|
||||
hostCAHash <- newEmptyTMVarIO
|
||||
pure RCHClient_ {startedPort, announcer, hostCAHash, endSession}
|
||||
runClient :: RCHClient_ -> RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> RCHostKeys -> ExceptT RCErrorType IO (Async ())
|
||||
runClient :: RCHClient_ -> RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> RCHostKeys -> IO (Async ())
|
||||
runClient RCHClient_ {startedPort, announcer, hostCAHash, endSession} r hostKeys = do
|
||||
tlsCreds <- liftIO $ genTLSCredentials drg caKey caCert
|
||||
tlsCreds <- genTLSCredentials drg caKey caCert
|
||||
startTLSServer port_ startedPort tlsCreds (tlsHooks r knownHost hostCAHash) $ \tls ->
|
||||
void . runExceptT $ do
|
||||
r' <- newEmptyTMVarIO
|
||||
@@ -265,7 +265,7 @@ connectRCCtrl_ :: TVar ChaChaDRG -> RCCtrlPairing -> RCInvitation -> J.Value ->
|
||||
connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca, host, port} hostAppInfo = do
|
||||
r <- newEmptyTMVarIO
|
||||
c <- liftIO mkClient
|
||||
action <- async $ runClient c r `putRCError` r
|
||||
action <- liftIO . async . void . runExceptT $ runClient c r `putRCError` r
|
||||
pure (RCCtrlClient {action, client_ = c}, r)
|
||||
where
|
||||
mkClient :: IO RCCClient_
|
||||
@@ -280,7 +280,7 @@ connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca,
|
||||
TLS.Credentials (creds : _) -> pure $ Just creds
|
||||
_ -> throwError $ RCEInternal "genTLSCredentials must generate credentials"
|
||||
let clientConfig = defaultTransportClientConfig {clientCredentials}
|
||||
runTransportClient clientConfig Nothing host (show port) (Just ca) $ \tls@TLS {tlsBuffer, tlsContext} -> do
|
||||
ExceptT . runTransportClient clientConfig Nothing host (show port) (Just ca) $ \tls@TLS {tlsBuffer, tlsContext} -> runExceptT $ do
|
||||
-- pump socket to detect connection problems
|
||||
liftIO $ peekBuffered tlsBuffer 100000 (TLS.recvData tlsContext) >>= logDebug . tshow -- should normally be ("", Nothing) here
|
||||
logDebug "Got TLS connection"
|
||||
@@ -360,11 +360,11 @@ prepareCtrlSession
|
||||
-- * Multicast discovery
|
||||
|
||||
announceRC :: TVar ChaChaDRG -> Int -> C.PrivateKeyEd25519 -> C.PublicKeyX25519 -> RCHostKeys -> RCInvitation -> ExceptT RCErrorType IO ()
|
||||
announceRC drg maxCount idPrivKey knownDhPub RCHostKeys {sessKeys, dhKeys} inv = withSender $ \sender -> do
|
||||
announceRC drg maxCount idPrivKey knownDhPub RCHostKeys {sessKeys, dhKeys} inv = ExceptT $ withSender $ \sender -> runExceptT $ do
|
||||
replicateM_ maxCount $ do
|
||||
logDebug "Announcing..."
|
||||
nonce <- atomically $ C.randomCbNonce drg
|
||||
encInvitation <- liftEitherWith undefined $ C.cbEncrypt sharedKey nonce sigInvitation encInvitationSize
|
||||
encInvitation <- liftEitherWith (const RCEEncrypt) $ C.cbEncrypt sharedKey nonce sigInvitation encInvitationSize
|
||||
liftIO . UDP.send sender $ smpEncode RCEncInvitation {dhPubKey, nonce, encInvitation}
|
||||
threadDelay 1000000
|
||||
where
|
||||
@@ -375,9 +375,9 @@ announceRC drg maxCount idPrivKey knownDhPub RCHostKeys {sessKeys, dhKeys} inv =
|
||||
|
||||
discoverRCCtrl :: TMVar Int -> NonEmpty RCCtrlPairing -> ExceptT RCErrorType IO (RCCtrlPairing, RCVerifiedInvitation)
|
||||
discoverRCCtrl subscribers pairings =
|
||||
timeoutThrow RCENotDiscovered 30000000 $ withListener subscribers $ \listener ->
|
||||
loop $ do
|
||||
(source, bytes) <- recvAnnounce listener
|
||||
timeoutThrow RCENotDiscovered 30000000 $ ExceptT $ withListener subscribers $ \listener ->
|
||||
runExceptT . loop $ do
|
||||
(source, bytes) <- liftIO $ recvAnnounce listener
|
||||
encInvitation <- liftEitherWith (const RCEInvitation) $ smpDecode bytes
|
||||
r@(_, RCVerifiedInvitation RCInvitation {host}) <- findRCCtrlPairing pairings encInvitation
|
||||
case source of
|
||||
@@ -386,10 +386,7 @@ discoverRCCtrl subscribers pairings =
|
||||
pure r
|
||||
where
|
||||
loop :: ExceptT RCErrorType IO a -> ExceptT RCErrorType IO a
|
||||
loop action =
|
||||
liftIO (runExceptT action) >>= \case
|
||||
Left err -> logError (tshow err) >> loop action
|
||||
Right res -> pure res
|
||||
loop action = action `catchRCError` \e -> logError (tshow e) >> loop action
|
||||
|
||||
findRCCtrlPairing :: NonEmpty RCCtrlPairing -> RCEncInvitation -> ExceptT RCErrorType IO (RCCtrlPairing, RCVerifiedInvitation)
|
||||
findRCCtrlPairing pairings RCEncInvitation {dhPubKey, nonce, encInvitation} = do
|
||||
|
||||
@@ -68,7 +68,7 @@ preferAddress RCCtrlAddress {address, interface} addrs =
|
||||
matchAddr RCCtrlAddress {address = a} = a == address
|
||||
matchIface RCCtrlAddress {interface = i} = i == interface
|
||||
|
||||
startTLSServer :: MonadUnliftIO m => Maybe Word16 -> TMVar (Maybe N.PortNumber) -> TLS.Credentials -> TLS.ServerHooks -> (Transport.TLS -> IO ()) -> m (Async ())
|
||||
startTLSServer :: Maybe Word16 -> TMVar (Maybe N.PortNumber) -> TLS.Credentials -> TLS.ServerHooks -> (Transport.TLS -> IO ()) -> IO (Async ())
|
||||
startTLSServer port_ startedOnPort credentials hooks server = async . liftIO $ do
|
||||
started <- newEmptyTMVarIO
|
||||
bracketOnError (startTCPServer started $ maybe "0" show port_) (\_e -> setPort Nothing) $ \socket ->
|
||||
@@ -91,14 +91,14 @@ startTLSServer port_ startedOnPort credentials hooks server = async . liftIO $ d
|
||||
TLS.serverSupported = supportedParameters
|
||||
}
|
||||
|
||||
withSender :: MonadUnliftIO m => (UDP.UDPSocket -> m a) -> m a
|
||||
withSender = bracket (liftIO $ UDP.clientSocket MULTICAST_ADDR_V4 DISCOVERY_PORT False) (liftIO . UDP.close)
|
||||
withSender :: (UDP.UDPSocket -> IO a) -> IO a
|
||||
withSender = bracket (UDP.clientSocket MULTICAST_ADDR_V4 DISCOVERY_PORT False) (UDP.close)
|
||||
|
||||
withListener :: MonadUnliftIO m => TMVar Int -> (UDP.ListenSocket -> m a) -> m a
|
||||
withListener :: TMVar Int -> (UDP.ListenSocket -> IO a) -> IO a
|
||||
withListener subscribers = bracket (openListener subscribers) (closeListener subscribers)
|
||||
|
||||
openListener :: MonadIO m => TMVar Int -> m UDP.ListenSocket
|
||||
openListener subscribers = liftIO $ do
|
||||
openListener :: TMVar Int -> IO UDP.ListenSocket
|
||||
openListener subscribers = do
|
||||
sock <- UDP.serverSocket (MULTICAST_ADDR_V4, read DISCOVERY_PORT)
|
||||
logDebug $ "Discovery listener socket: " <> tshow sock
|
||||
let raw = UDP.listenSocket sock
|
||||
@@ -106,10 +106,9 @@ openListener subscribers = liftIO $ do
|
||||
joinMulticast subscribers raw (listenerHostAddr4 sock)
|
||||
pure sock
|
||||
|
||||
closeListener :: MonadIO m => TMVar Int -> UDP.ListenSocket -> m ()
|
||||
closeListener :: TMVar Int -> UDP.ListenSocket -> IO ()
|
||||
closeListener subscribers sock =
|
||||
liftIO $
|
||||
partMulticast subscribers (UDP.listenSocket sock) (listenerHostAddr4 sock) `finally` UDP.stop sock
|
||||
partMulticast subscribers (UDP.listenSocket sock) (listenerHostAddr4 sock) `finally` UDP.stop sock
|
||||
|
||||
joinMulticast :: TMVar Int -> N.Socket -> N.HostAddress -> IO ()
|
||||
joinMulticast subscribers sock group = do
|
||||
@@ -132,7 +131,7 @@ listenerHostAddr4 sock = case UDP.mySockAddr sock of
|
||||
N.SockAddrInet _port host -> host
|
||||
_ -> error "MULTICAST_ADDR_V4 is V4"
|
||||
|
||||
recvAnnounce :: MonadIO m => UDP.ListenSocket -> m (N.SockAddr, ByteString)
|
||||
recvAnnounce sock = liftIO $ do
|
||||
recvAnnounce :: UDP.ListenSocket -> IO (N.SockAddr, ByteString)
|
||||
recvAnnounce sock = do
|
||||
(invite, UDP.ClientSockAddr source _cmsg) <- UDP.recvFrom sock
|
||||
pure (source, invite)
|
||||
|
||||
@@ -238,17 +238,6 @@ type SessionCode = ByteString
|
||||
|
||||
type RCStepTMVar a = TMVar (Either RCErrorType a)
|
||||
|
||||
type Tasks = TVar [Async ()]
|
||||
|
||||
asyncRegistered :: MonadUnliftIO m => Tasks -> m () -> m ()
|
||||
asyncRegistered tasks action = async action >>= registerAsync tasks
|
||||
|
||||
registerAsync :: MonadIO m => Tasks -> Async () -> m ()
|
||||
registerAsync tasks = atomically . modifyTVar tasks . (:)
|
||||
|
||||
cancelTasks :: MonadIO m => Tasks -> m ()
|
||||
cancelTasks tasks = readTVarIO tasks >>= mapM_ cancel
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCE") ''RCErrorType)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RCCtrlAddress)
|
||||
|
||||
+9
-5
@@ -13,7 +13,7 @@ module AgentTests (agentTests) where
|
||||
|
||||
import AgentTests.ConnectionRequestTests
|
||||
import AgentTests.DoubleRatchetTests (doubleRatchetTests)
|
||||
import AgentTests.FunctionalAPITests (functionalAPITests, pattern Msg, pattern Msg')
|
||||
import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg')
|
||||
import AgentTests.MigrationTests (migrationTests)
|
||||
import AgentTests.NotificationTests (notificationTests)
|
||||
import AgentTests.SQLiteTests (storeTests)
|
||||
@@ -194,8 +194,8 @@ pqMatrix2_ pqInv _ smpTest test = do
|
||||
|
||||
pqMatrix3 ::
|
||||
HasCallStack =>
|
||||
TProxy c ->
|
||||
(HasCallStack => (c -> c -> c -> IO ()) -> Expectation) ->
|
||||
TProxy c ->
|
||||
(HasCallStack => (c -> c -> c -> IO ()) -> Expectation) ->
|
||||
(HasCallStack => (c, InitialKeys) -> (c, PQSupport) -> (c, PQSupport) -> IO ()) ->
|
||||
Spec
|
||||
pqMatrix3 _ smpTest test = do
|
||||
@@ -452,7 +452,7 @@ testServerConnectionAfterError t _ = do
|
||||
where
|
||||
server = SMPServer "localhost" testPort2 testKeyHash
|
||||
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
|
||||
withAgent1 = withAgent agentTestPort testDB 0
|
||||
withAgent1 = withAgent agentTestPort testDB 0
|
||||
withAgent2 = withAgent agentTestPort2 testDB2 10
|
||||
withAgent :: String -> FilePath -> Int -> (c -> IO a) -> IO a
|
||||
withAgent agentPort agentDB initClientId = withSmpAgentThreadOn_ (ATransport t) (agentPort, testPort2, agentDB) initClientId (pure ()) . const . testSMPAgentClientOn agentPort
|
||||
@@ -554,7 +554,11 @@ testResumeDeliveryQuotaExceeded _ alice bob = do
|
||||
alice #:# "the last message not sent"
|
||||
bob <#= \case ("", "alice", Msg "message 4") -> True; _ -> False
|
||||
bob #: ("4", "alice", "ACK 7") #> ("4", "alice", OK)
|
||||
alice <# ("", "bob", SENT 8)
|
||||
inAnyOrder
|
||||
(tGetAgent alice)
|
||||
[ \case ("", c, Right (SENT 8)) -> c == "bob"; _ -> False,
|
||||
\case ("", c, Right QCONT) -> c == "bob"; _ -> False
|
||||
]
|
||||
bob <#= \case ("", "alice", Msg "over quota") -> True; _ -> False
|
||||
-- message 8 is skipped because of alice agent sending "QCONT" message
|
||||
bob #: ("5", "alice", "ACK 9") #> ("5", "alice", OK)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,10 @@ import AgentTests.FunctionalAPITests
|
||||
createConnection,
|
||||
exchangeGreetingsMsgId,
|
||||
get,
|
||||
getSMPAgentClient',
|
||||
withAgent,
|
||||
withAgentClients2,
|
||||
withAgentClientsCfgServers2,
|
||||
withAgentClients3,
|
||||
joinConnection,
|
||||
makeConnection,
|
||||
nGet,
|
||||
@@ -47,7 +50,7 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import NtfClient
|
||||
import SMPAgentClient (agentCfg, initAgentServers, initAgentServers2, testDB, testDB2, testDB3, testNtfServer, testNtfServer2)
|
||||
import SMPAgentClient (agentCfg, initAgentServers, initAgentServers2, testDB, testDB2, testNtfServer, testNtfServer2)
|
||||
import SMPClient (cfg, cfgV7, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn)
|
||||
import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage)
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore')
|
||||
@@ -165,8 +168,7 @@ runNtfTestCfg t smpCfg ntfCfg aCfg bCfg runTest =
|
||||
|
||||
testNotificationToken :: APNSMockServer -> IO ()
|
||||
testNotificationToken APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
runRight_ $ do
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
@@ -179,7 +181,7 @@ testNotificationToken APNSMockServer {apnsQ} = do
|
||||
deleteNtfToken a tkn
|
||||
-- agent deleted this token
|
||||
Left (CMD PROHIBITED) <- tryE $ checkNtfToken a tkn
|
||||
disconnectAgentClient a
|
||||
pure ()
|
||||
|
||||
(.->) :: J.Value -> J.Key -> ExceptT AgentErrorType IO ByteString
|
||||
v .-> key = do
|
||||
@@ -193,8 +195,7 @@ testNtfTokenRepeatRegistration :: APNSMockServer -> IO ()
|
||||
testNtfTokenRepeatRegistration APNSMockServer {apnsQ} = do
|
||||
-- setLogLevel LogError -- LogDebug
|
||||
-- withGlobalLogging logCfg $ do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
runRight_ $ do
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
@@ -211,15 +212,13 @@ testNtfTokenRepeatRegistration APNSMockServer {apnsQ} = do
|
||||
-- can still use the first verification code, it is the same after decryption
|
||||
verifyNtfToken a tkn nonce verification
|
||||
NTActive <- checkNtfToken a tkn
|
||||
disconnectAgentClient a
|
||||
pure ()
|
||||
|
||||
testNtfTokenSecondRegistration :: APNSMockServer -> IO ()
|
||||
testNtfTokenSecondRegistration APNSMockServer {apnsQ} = do
|
||||
testNtfTokenSecondRegistration APNSMockServer {apnsQ} =
|
||||
-- setLogLevel LogError -- LogDebug
|
||||
-- withGlobalLogging logCfg $ do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
a' <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ $ do
|
||||
withAgentClients2 $ \a a' -> runRight_ $ do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
@@ -247,107 +246,101 @@ testNtfTokenSecondRegistration APNSMockServer {apnsQ} = do
|
||||
Left (NTF AUTH) <- tryE $ checkNtfToken a tkn
|
||||
-- and the second is active
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
disconnectAgentClient a
|
||||
disconnectAgentClient a'
|
||||
pure ()
|
||||
|
||||
testNtfTokenServerRestart :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
ntfData <- withNtfServer t . runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
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
|
||||
ntfData <- withAgent 1 agentCfg initAgentServers testDB $ \a ->
|
||||
withNtfServer t . runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
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
|
||||
threadDelay 1000000
|
||||
disconnectAgentClient a
|
||||
a' <- getSMPAgentClient' 2 agentCfg initAgentServers testDB
|
||||
-- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
|
||||
-- so that repeat verification happens without restarting the clients, when notification arrives
|
||||
withNtfServer t . runRight_ $ do
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
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"
|
||||
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
|
||||
liftIO $ sendApnsResponse' APNSRespOk
|
||||
verifyNtfToken a' tkn nonce' verification'
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
disconnectAgentClient a'
|
||||
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,
|
||||
-- so that repeat verification happens without restarting the clients, when notification arrives
|
||||
withNtfServer t . runRight_ $ do
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
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"
|
||||
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
|
||||
liftIO $ sendApnsResponse' APNSRespOk
|
||||
verifyNtfToken a' tkn nonce' verification'
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
|
||||
getTestNtfTokenPort :: (MonadUnliftIO m, MonadError AgentErrorType m) => AgentClient -> m String
|
||||
getTestNtfTokenPort :: AgentClient -> AE String
|
||||
getTestNtfTokenPort a =
|
||||
runReaderT (withStore' a getSavedNtfToken) (agentEnv a) >>= \case
|
||||
ExceptT (runExceptT (withStore' a getSavedNtfToken) `runReaderT` agentEnv a) >>= \case
|
||||
Just NtfToken {ntfServer = ProtocolServer {port}} -> pure port
|
||||
Nothing -> error "no active NtfToken"
|
||||
|
||||
testNtfTokenMultipleServers :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenMultipleServers t APNSMockServer {apnsQ} = do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers2 testDB
|
||||
withNtfServerThreadOn t ntfTestPort $ \ntf ->
|
||||
withNtfServerThreadOn t ntfTestPort2 $ \ntf2 -> runRight_ $ do
|
||||
-- register a new token, the agent picks a server and stores its choice
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
verifyNtfToken a tkn nonce verification
|
||||
NTActive <- checkNtfToken a tkn
|
||||
-- shut down the "other" server
|
||||
port <- getTestNtfTokenPort a
|
||||
liftIO . killThread $ if port == ntfTestPort then ntf2 else ntf
|
||||
-- still works
|
||||
NTActive <- checkNtfToken a tkn
|
||||
liftIO . killThread $ if port == ntfTestPort then ntf else ntf2
|
||||
-- negative test, the correct server is now gone
|
||||
Left _ <- tryError (checkNtfToken a tkn)
|
||||
pure ()
|
||||
withAgent 1 agentCfg initAgentServers2 testDB $ \a ->
|
||||
withNtfServerThreadOn t ntfTestPort $ \ntf ->
|
||||
withNtfServerThreadOn t ntfTestPort2 $ \ntf2 -> runRight_ $ do
|
||||
-- register a new token, the agent picks a server and stores its choice
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
verifyNtfToken a tkn nonce verification
|
||||
NTActive <- checkNtfToken a tkn
|
||||
-- shut down the "other" server
|
||||
port <- getTestNtfTokenPort a
|
||||
liftIO . killThread $ if port == ntfTestPort then ntf2 else ntf
|
||||
-- still works
|
||||
NTActive <- checkNtfToken a tkn
|
||||
liftIO . killThread $ if port == ntfTestPort then ntf else ntf2
|
||||
-- negative test, the correct server is now gone
|
||||
Left _ <- tryError (checkNtfToken a tkn)
|
||||
pure ()
|
||||
|
||||
testNtfTokenChangeServers :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenChangeServers t APNSMockServer {apnsQ} =
|
||||
withNtfServerThreadOn t ntfTestPort $ \ntf -> do
|
||||
tkn1 <- runRight $ do
|
||||
a <- liftIO $ getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
tkn1 <- withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight $ do
|
||||
tkn <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
NTActive <- checkNtfToken a tkn
|
||||
setNtfServers a [testNtfServer2]
|
||||
liftIO $ setNtfServers a [testNtfServer2]
|
||||
NTActive <- checkNtfToken a tkn -- still works on old server
|
||||
disconnectAgentClient a
|
||||
pure tkn
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
a <- liftIO $ getSMPAgentClient' 2 agentCfg initAgentServers testDB
|
||||
runRight_ $ do
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort
|
||||
NTActive <- checkNtfToken a tkn1
|
||||
setNtfServers a [testNtfServer2] -- just change configured server list
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort -- not yet changed
|
||||
-- trigger token replace
|
||||
tkn2 <- registerTestToken a "xyzw" NMInstant apnsQ
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort -- not yet changed
|
||||
deleteNtfToken a tkn2 -- force server switch
|
||||
Left BROKER {brokerErr = NETWORK} <- tryError $ registerTestToken a "qwer" NMInstant apnsQ -- ok, it's down for now
|
||||
getTestNtfTokenPort a >>= \port2 -> liftIO $ port2 `shouldBe` ntfTestPort2 -- but the token got updated
|
||||
killThread ntf
|
||||
withNtfServerOn t ntfTestPort2 $ runRight_ $ do
|
||||
tkn <- registerTestToken a "qwer" NMInstant apnsQ
|
||||
checkNtfToken a tkn >>= \r -> liftIO $ r `shouldBe` NTActive
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a -> do
|
||||
runRight_ $ do
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort
|
||||
NTActive <- checkNtfToken a tkn1
|
||||
liftIO $ setNtfServers a [testNtfServer2] -- just change configured server list
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort -- not yet changed
|
||||
-- trigger token replace
|
||||
tkn2 <- registerTestToken a "xyzw" NMInstant apnsQ
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort -- not yet changed
|
||||
deleteNtfToken a tkn2 -- force server switch
|
||||
Left BROKER {brokerErr = NETWORK} <- tryError $ registerTestToken a "qwer" NMInstant apnsQ -- ok, it's down for now
|
||||
getTestNtfTokenPort a >>= \port2 -> liftIO $ port2 `shouldBe` ntfTestPort2 -- but the token got updated
|
||||
killThread ntf
|
||||
withNtfServerOn t ntfTestPort2 $ runRight_ $ do
|
||||
tkn <- registerTestToken a "qwer" NMInstant apnsQ
|
||||
checkNtfToken a tkn >>= \r -> liftIO $ r `shouldBe` NTActive
|
||||
|
||||
testRunNTFServerTests :: ATransport -> NtfServer -> IO (Maybe ProtocolTestFailure)
|
||||
testRunNTFServerTests t srv =
|
||||
withNtfServerThreadOn t ntfTestPort $ \ntf -> do
|
||||
a <- liftIO $ getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
r <- runRight $ testProtocolServer a 1 $ ProtoServerWithAuth srv Nothing
|
||||
killThread ntf
|
||||
pure r
|
||||
withNtfServerOn t ntfTestPort $
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a ->
|
||||
testProtocolServer a 1 $ ProtoServerWithAuth srv Nothing
|
||||
|
||||
testNotificationSubscriptionExistingConnection :: APNSMockServer -> AgentClient -> AgentClient -> IO ()
|
||||
testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@AgentClient {agentEnv = Env {config = aliceCfg}} bob = do
|
||||
@@ -382,11 +375,9 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@Agen
|
||||
Left (CMD PROHIBITED) <- runExceptT $ getNotificationMessage alice nonce message
|
||||
|
||||
-- aliceNtf client doesn't have subscription and is allowed to get notification message
|
||||
aliceNtf <- getSMPAgentClient' 3 aliceCfg initAgentServers testDB
|
||||
runRight_ $ do
|
||||
withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do
|
||||
(_, [SMPMsgMeta {msgFlags = MsgFlags True}]) <- getNotificationMessage aliceNtf nonce message
|
||||
pure ()
|
||||
disconnectAgentClient aliceNtf
|
||||
|
||||
runRight_ $ do
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
@@ -458,10 +449,8 @@ registerTestToken a token mode apnsQ = do
|
||||
pure tkn
|
||||
|
||||
testChangeNotificationsMode :: APNSMockServer -> IO ()
|
||||
testChangeNotificationsMode APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
bob <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ $ do
|
||||
testChangeNotificationsMode APNSMockServer {apnsQ} =
|
||||
withAgentClients2 $ \alice bob -> runRight_ $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
@@ -517,17 +506,13 @@ testChangeNotificationsMode APNSMockServer {apnsQ} = do
|
||||
ackMessage alice bobId (baseId + 5) Nothing
|
||||
-- no notifications should follow
|
||||
noNotification apnsQ
|
||||
disconnectAgentClient alice
|
||||
disconnectAgentClient bob
|
||||
where
|
||||
baseId = 3
|
||||
msgId = subtract baseId
|
||||
|
||||
testChangeToken :: APNSMockServer -> IO ()
|
||||
testChangeToken APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
bob <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
(aliceId, bobId) <- runRight $ do
|
||||
testChangeToken APNSMockServer {apnsQ} = withAgent 1 agentCfg initAgentServers testDB2 $ \bob -> do
|
||||
(aliceId, bobId) <- withAgent 2 agentCfg initAgentServers testDB $ \alice -> runRight $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
@@ -546,10 +531,8 @@ testChangeToken APNSMockServer {apnsQ} = do
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId (baseId + 1) Nothing
|
||||
pure (aliceId, bobId)
|
||||
disconnectAgentClient alice
|
||||
|
||||
alice1 <- getSMPAgentClient' 3 agentCfg initAgentServers testDB
|
||||
runRight_ $ do
|
||||
withAgent 3 agentCfg initAgentServers testDB $ \alice1 -> runRight_ $ do
|
||||
subscribeConnection alice1 bobId
|
||||
-- change notification token
|
||||
void $ registerTestToken alice1 "bcde" NMInstant apnsQ
|
||||
@@ -562,16 +545,12 @@ testChangeToken APNSMockServer {apnsQ} = do
|
||||
ackMessage alice1 bobId (baseId + 2) Nothing
|
||||
-- no notifications should follow
|
||||
noNotification apnsQ
|
||||
disconnectAgentClient alice1
|
||||
disconnectAgentClient bob
|
||||
where
|
||||
baseId = 3
|
||||
msgId = subtract baseId
|
||||
|
||||
testNotificationsStoreLog :: ATransport -> APNSMockServer -> IO ()
|
||||
testNotificationsStoreLog t APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
bob <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
testNotificationsStoreLog t APNSMockServer {apnsQ} = withAgentClients2 $ \alice bob -> do
|
||||
(aliceId, bobId) <- withNtfServerStoreLog t $ \threadId -> runRight $ do
|
||||
(aliceId, bobId) <- makeConnection alice bob
|
||||
_ <- registerTestToken alice "abcd" NMInstant apnsQ
|
||||
@@ -593,13 +572,9 @@ testNotificationsStoreLog t APNSMockServer {apnsQ} = do
|
||||
void $ messageNotificationData alice apnsQ
|
||||
get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
|
||||
liftIO $ killThread threadId
|
||||
disconnectAgentClient alice
|
||||
disconnectAgentClient bob
|
||||
|
||||
testNotificationsSMPRestart :: ATransport -> APNSMockServer -> IO ()
|
||||
testNotificationsSMPRestart t APNSMockServer {apnsQ} = do
|
||||
alice <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
bob <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
testNotificationsSMPRestart t APNSMockServer {apnsQ} = withAgentClients2 $ \alice bob -> do
|
||||
(aliceId, bobId) <- withSmpServerStoreLogOn t testPort $ \threadId -> runRight $ do
|
||||
(aliceId, bobId) <- makeConnection alice bob
|
||||
_ <- registerTestToken alice "abcd" NMInstant apnsQ
|
||||
@@ -625,49 +600,44 @@ testNotificationsSMPRestart t APNSMockServer {apnsQ} = do
|
||||
_ <- messageNotificationData alice apnsQ
|
||||
get alice =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
|
||||
liftIO $ killThread threadId
|
||||
disconnectAgentClient alice
|
||||
disconnectAgentClient bob
|
||||
|
||||
testNotificationsSMPRestartBatch :: Int -> ATransport -> APNSMockServer -> IO ()
|
||||
testNotificationsSMPRestartBatch n t APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers2 testDB
|
||||
b <- getSMPAgentClient' 2 agentCfg initAgentServers2 testDB2
|
||||
threadDelay 1000000
|
||||
conns <- runServers $ do
|
||||
conns <- replicateM (n :: Int) $ makeConnection a b
|
||||
_ <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
liftIO $ threadDelay 5000000
|
||||
forM_ conns $ \(aliceId, bobId) -> do
|
||||
msgId <- sendMessage b aliceId (SMP.MsgFlags True) "hello"
|
||||
get b ##> ("", aliceId, SENT msgId)
|
||||
void $ messageNotificationData a apnsQ
|
||||
get a =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage a bobId msgId Nothing
|
||||
pure conns
|
||||
testNotificationsSMPRestartBatch n t APNSMockServer {apnsQ} =
|
||||
withAgentClientsCfgServers2 agentCfg agentCfg initAgentServers2 $ \a b -> do
|
||||
threadDelay 1000000
|
||||
conns <- runServers $ do
|
||||
conns <- replicateM (n :: Int) $ makeConnection a b
|
||||
_ <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
liftIO $ threadDelay 5000000
|
||||
forM_ conns $ \(aliceId, bobId) -> do
|
||||
msgId <- sendMessage b aliceId (SMP.MsgFlags True) "hello"
|
||||
get b ##> ("", aliceId, SENT msgId)
|
||||
void $ messageNotificationData a apnsQ
|
||||
get a =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage a bobId msgId Nothing
|
||||
pure conns
|
||||
|
||||
runRight_ @AgentErrorType $ do
|
||||
("", "", DOWN _ bcs1) <- nGet a
|
||||
("", "", DOWN _ bcs2) <- nGet a
|
||||
liftIO $ length (bcs1 <> bcs2) `shouldBe` length conns
|
||||
("", "", DOWN _ acs1) <- nGet b
|
||||
("", "", DOWN _ acs2) <- nGet b
|
||||
liftIO $ length (acs1 <> acs2) `shouldBe` length conns
|
||||
runRight_ @AgentErrorType $ do
|
||||
("", "", DOWN _ bcs1) <- nGet a
|
||||
("", "", DOWN _ bcs2) <- nGet a
|
||||
liftIO $ length (bcs1 <> bcs2) `shouldBe` length conns
|
||||
("", "", DOWN _ acs1) <- nGet b
|
||||
("", "", DOWN _ acs2) <- nGet b
|
||||
liftIO $ length (acs1 <> acs2) `shouldBe` length conns
|
||||
|
||||
runServers $ do
|
||||
("", "", UP _ bcs1) <- nGet a
|
||||
("", "", UP _ bcs2) <- nGet a
|
||||
liftIO $ length (bcs1 <> bcs2) `shouldBe` length conns
|
||||
("", "", UP _ acs1) <- nGet b
|
||||
("", "", UP _ acs2) <- nGet b
|
||||
liftIO $ length (acs1 <> acs2) `shouldBe` length conns
|
||||
liftIO $ threadDelay 1500000
|
||||
forM_ conns $ \(aliceId, bobId) -> do
|
||||
msgId <- sendMessage b aliceId (SMP.MsgFlags True) "hello again"
|
||||
get b ##> ("", aliceId, SENT msgId)
|
||||
_ <- messageNotificationData a apnsQ
|
||||
get a =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
|
||||
disconnectAgentClient a
|
||||
disconnectAgentClient b
|
||||
runServers $ do
|
||||
("", "", UP _ bcs1) <- nGet a
|
||||
("", "", UP _ bcs2) <- nGet a
|
||||
liftIO $ length (bcs1 <> bcs2) `shouldBe` length conns
|
||||
("", "", UP _ acs1) <- nGet b
|
||||
("", "", UP _ acs2) <- nGet b
|
||||
liftIO $ length (acs1 <> acs2) `shouldBe` length conns
|
||||
liftIO $ threadDelay 1500000
|
||||
forM_ conns $ \(aliceId, bobId) -> do
|
||||
msgId <- sendMessage b aliceId (SMP.MsgFlags True) "hello again"
|
||||
get b ##> ("", aliceId, SENT msgId)
|
||||
_ <- messageNotificationData a apnsQ
|
||||
get a =##> \case ("", c, Msg "hello again") -> c == bobId; _ -> False
|
||||
where
|
||||
runServers :: ExceptT AgentErrorType IO a -> IO a
|
||||
runServers a = do
|
||||
@@ -678,10 +648,8 @@ testNotificationsSMPRestartBatch n t APNSMockServer {apnsQ} = do
|
||||
pure res
|
||||
|
||||
testSwitchNotifications :: InitialAgentServers -> APNSMockServer -> IO ()
|
||||
testSwitchNotifications servers APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient' 1 agentCfg servers testDB
|
||||
b <- getSMPAgentClient' 2 agentCfg servers testDB2
|
||||
runRight_ $ do
|
||||
testSwitchNotifications servers APNSMockServer {apnsQ} =
|
||||
withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do
|
||||
(aId, bId) <- makeConnection a b
|
||||
exchangeGreetingsMsgId 4 a bId b aId
|
||||
_ <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
@@ -697,22 +665,17 @@ testSwitchNotifications servers APNSMockServer {apnsQ} = do
|
||||
switchComplete a bId b aId
|
||||
liftIO $ threadDelay 500000
|
||||
testMessage "hello again"
|
||||
disconnectAgentClient a
|
||||
disconnectAgentClient b
|
||||
|
||||
testNotificationsOldToken :: APNSMockServer -> IO ()
|
||||
testNotificationsOldToken APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
b <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
c <- getSMPAgentClient' 3 agentCfg initAgentServers testDB3
|
||||
runRight_ $ do
|
||||
testNotificationsOldToken APNSMockServer {apnsQ} =
|
||||
withAgentClients3 $ \a b c -> runRight_ $ do
|
||||
(abId, baId) <- makeConnection a b
|
||||
let testMessageAB = testMessage_ apnsQ a abId b baId
|
||||
_ <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
liftIO $ threadDelay 250000
|
||||
testMessageAB "hello"
|
||||
-- change server
|
||||
setNtfServers a [testNtfServer2] -- server 2 isn't running now, don't use
|
||||
liftIO $ setNtfServers a [testNtfServer2] -- server 2 isn't running now, don't use
|
||||
-- replacing token keeps server
|
||||
_ <- registerTestToken a "xyzw" NMInstant apnsQ
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort
|
||||
@@ -721,16 +684,10 @@ testNotificationsOldToken APNSMockServer {apnsQ} = do
|
||||
(acId, caId) <- makeConnection a c
|
||||
let testMessageAC = testMessage_ apnsQ a acId c caId
|
||||
testMessageAC "greetings"
|
||||
disconnectAgentClient a
|
||||
disconnectAgentClient b
|
||||
disconnectAgentClient c
|
||||
|
||||
testNotificationsNewToken :: APNSMockServer -> ThreadId -> IO ()
|
||||
testNotificationsNewToken APNSMockServer {apnsQ} oldNtf = do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
b <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
c <- getSMPAgentClient' 3 agentCfg initAgentServers testDB3
|
||||
runRight_ $ do
|
||||
testNotificationsNewToken APNSMockServer {apnsQ} oldNtf =
|
||||
withAgentClients3 $ \a b c -> runRight_ $ do
|
||||
(abId, baId) <- makeConnection a b
|
||||
let testMessageAB = testMessage_ apnsQ a abId b baId
|
||||
tkn <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
@@ -738,7 +695,7 @@ testNotificationsNewToken APNSMockServer {apnsQ} oldNtf = do
|
||||
liftIO $ threadDelay 250000
|
||||
testMessageAB "hello"
|
||||
-- switch
|
||||
setNtfServers a [testNtfServer2]
|
||||
liftIO $ setNtfServers a [testNtfServer2]
|
||||
deleteNtfToken a tkn
|
||||
_ <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
getTestNtfTokenPort a >>= \port -> liftIO $ port `shouldBe` ntfTestPort2
|
||||
@@ -749,9 +706,6 @@ testNotificationsNewToken APNSMockServer {apnsQ} oldNtf = do
|
||||
(acId, caId) <- makeConnection a c
|
||||
let testMessageAC = testMessage_ apnsQ a acId c caId
|
||||
testMessageAC "greetings"
|
||||
disconnectAgentClient a
|
||||
disconnectAgentClient b
|
||||
disconnectAgentClient c
|
||||
|
||||
testMessage_ :: HasCallStack => TBQueue APNSMockRequest -> AgentClient -> ConnId -> AgentClient -> ConnId -> SMP.MsgBody -> ExceptT AgentErrorType IO ()
|
||||
testMessage_ apnsQ a aId b bId msg = do
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module CoreTests.BatchingTests (batchingTests) where
|
||||
|
||||
@@ -11,6 +13,9 @@ import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.CertificateStore as XS
|
||||
import qualified Data.X509.File as XF
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
@@ -276,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
|
||||
@@ -306,15 +311,15 @@ 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
|
||||
testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient
|
||||
testTHandleParams v sessionId =
|
||||
THandleParams
|
||||
{ sessionId,
|
||||
@@ -325,11 +330,16 @@ testTHandleParams v sessionId =
|
||||
batch = True
|
||||
}
|
||||
|
||||
testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe THandleAuth)
|
||||
testTHandleAuth v g (C.APublicAuthKey a k) = case a of
|
||||
testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe (THandleAuth 'TClient))
|
||||
testTHandleAuth v g (C.APublicAuthKey a serverPeerPubKey) = case a of
|
||||
C.SX25519 | v >= authCmdsSMPVersion -> do
|
||||
(_, privKey) <- atomically $ C.generateKeyPair g
|
||||
pure $ Just THandleAuth {peerPubKey = k, privKey}
|
||||
ca <- head <$> XS.readCertificates "tests/fixtures/ca.crt"
|
||||
serverCert <- head <$> XS.readCertificates "tests/fixtures/server.crt"
|
||||
serverKey <- head <$> XF.readKeyFile "tests/fixtures/server.key"
|
||||
signKey <- either error pure $ C.x509ToPrivate (serverKey, []) >>= C.privKey @C.APrivateSignKey
|
||||
(serverAuthPub, _) <- atomically $ C.generateKeyPair @'C.X25519 g
|
||||
let serverCertKey = (X.CertificateChain [serverCert, ca], C.signX509 signKey $ C.toPubKey C.publicToX509 serverAuthPub)
|
||||
pure $ Just THAuthClient {serverPeerPubKey, serverCertKey, sessSecret = Nothing}
|
||||
_ -> pure Nothing
|
||||
|
||||
randomSENDCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> Int -> IO (PCTransmission ErrorType BrokerMsg)
|
||||
|
||||
@@ -16,9 +16,14 @@ import Data.Text.Encoding (encodeUtf8)
|
||||
import qualified Data.Text.Lazy as LT
|
||||
import qualified Data.Text.Lazy.Encoding as LE
|
||||
import Data.Type.Equality
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.CertificateStore as XS
|
||||
import qualified Data.X509.Validation as XV
|
||||
import qualified SMPClient
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Transport.Client
|
||||
import Test.Hspec
|
||||
import Test.Hspec.QuickCheck (modifyMaxSuccess)
|
||||
import Test.QuickCheck
|
||||
@@ -91,6 +96,8 @@ cryptoTests = do
|
||||
describe "Ed448" $ testEncoding C.SEd448
|
||||
describe "X25519" $ testEncoding C.SX25519
|
||||
describe "X448" $ testEncoding C.SX448
|
||||
describe "X509 chains" $ do
|
||||
it "should validate certificates" testValidateX509
|
||||
describe "sntrup761" $
|
||||
it "should enc/dec key" testSNTRUP761
|
||||
|
||||
@@ -223,6 +230,39 @@ testEncoding alg = it "should encode / decode key" . ioProperty $ do
|
||||
C.decodePubKey (C.encodePubKey k) == Right k
|
||||
&& C.decodePrivKey (C.encodePrivKey pk) == Right pk
|
||||
|
||||
testValidateX509 :: IO ()
|
||||
testValidateX509 = do
|
||||
let checkChain = validateCertificateChain SMPClient.testKeyHash "localhost" "5223" . X.CertificateChain
|
||||
checkChain [] `shouldReturn` [XV.EmptyChain]
|
||||
|
||||
caCreds <- XS.readCertificates "tests/fixtures/ca.crt"
|
||||
caCreds `shouldNotBe` []
|
||||
let ca = head caCreds
|
||||
|
||||
serverCreds <- XS.readCertificates "tests/fixtures/server.crt"
|
||||
serverCreds `shouldNotBe` []
|
||||
let server = head serverCreds
|
||||
checkChain [server, ca] `shouldReturn` []
|
||||
|
||||
ca2Creds <- XS.readCertificates "tests/fixtures/ca2.crt"
|
||||
ca2Creds `shouldNotBe` []
|
||||
let ca2 = head ca2Creds
|
||||
|
||||
-- signed by another CA
|
||||
server2Creds <- XS.readCertificates "tests/fixtures/server2.crt"
|
||||
server2Creds `shouldNotBe` []
|
||||
let server2 = head server2Creds
|
||||
checkChain [server2, ca2] `shouldReturn` [XV.UnknownCA]
|
||||
|
||||
-- messed up key rotation or other configuration problems
|
||||
checkChain [server2, ca] `shouldReturn` [XV.InvalidSignature XV.SignatureInvalid]
|
||||
|
||||
-- self-signed, unrelated to CA
|
||||
ssCreds <- XS.readCertificates "tests/fixtures/ss.crt"
|
||||
ssCreds `shouldNotBe` []
|
||||
let ss = head ssCreds
|
||||
checkChain [ss, ca] `shouldReturn` [XV.SelfSigned]
|
||||
|
||||
testSNTRUP761 :: IO ()
|
||||
testSNTRUP761 = do
|
||||
drg <- C.newRandom
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module CoreTests.ProtocolErrorTests where
|
||||
@@ -14,9 +13,11 @@ 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.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..))
|
||||
import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..), ProxyError (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..))
|
||||
import Simplex.RemoteControl.Types (RCErrorType (..))
|
||||
import Test.Hspec
|
||||
@@ -28,15 +29,19 @@ protocolErrorTests = modifyMaxSuccess (const 1000) $ do
|
||||
describe "errors parsing / serializing" $ do
|
||||
it "should parse SMP protocol errors" . property $ \(err :: ErrorType) ->
|
||||
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 possible $ \err ->
|
||||
strDecode (strEncode err) == Right err
|
||||
where
|
||||
errHasSpaces = \case
|
||||
BROKER srv (RESPONSE e) -> hasSpaces srv || hasSpaces e
|
||||
BROKER srv _ -> hasSpaces srv
|
||||
_ -> False
|
||||
possible :: Gen AgentErrorType
|
||||
possible =
|
||||
arbitrary >>= \case
|
||||
BROKER srv (Agent.RESPONSE e) | hasSpaces srv || hasSpaces e -> discard
|
||||
BROKER srv _ | hasSpaces srv -> discard
|
||||
SMP (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard
|
||||
NTF (PROXY (SMP.UNEXPECTED s)) | hasUnicode s -> discard
|
||||
ok -> pure ok
|
||||
hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s)
|
||||
hasUnicode = any (>= '\255')
|
||||
|
||||
deriving instance Generic AgentErrorType
|
||||
|
||||
@@ -54,6 +59,8 @@ deriving instance Generic ErrorType
|
||||
|
||||
deriving instance Generic CommandError
|
||||
|
||||
deriving instance Generic ProxyError
|
||||
|
||||
deriving instance Generic TransportError
|
||||
|
||||
deriving instance Generic HandshakeError
|
||||
@@ -78,6 +85,8 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary CommandError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary ProxyError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary TransportError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
|
||||
|
||||
+6
-9
@@ -16,7 +16,6 @@ module NtfClient where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Types as JT
|
||||
@@ -71,13 +70,11 @@ testKeyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI="
|
||||
ntfTestStoreLogFile :: FilePath
|
||||
ntfTestStoreLogFile = "tests/tmp/ntf-server-store.log"
|
||||
|
||||
testNtfClient :: (Transport c, MonadUnliftIO m, MonadFail m) => (THandleNTF c -> m a) -> m a
|
||||
testNtfClient :: Transport c => (THandleNTF c 'TClient -> IO a) -> IO a
|
||||
testNtfClient client = do
|
||||
Right host <- pure $ chooseTransportHost defaultNetworkConfig testHost
|
||||
runTransportClient defaultTransportClientConfig Nothing host ntfTestPort (Just testKeyHash) $ \h -> do
|
||||
g <- liftIO C.newRandom
|
||||
ks <- atomically $ C.generateKeyPair g
|
||||
liftIO (runExceptT $ ntfClientHandshake h ks testKeyHash supportedClientNTFVRange) >>= \case
|
||||
runTransportClient defaultTransportClientConfig Nothing host ntfTestPort (Just testKeyHash) $ \h ->
|
||||
runExceptT (ntfClientHandshake h testKeyHash supportedClientNTFVRange) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
|
||||
@@ -140,7 +137,7 @@ withNtfServerOn t port' = withNtfServerThreadOn t port' . const
|
||||
withNtfServer :: ATransport -> IO a -> IO a
|
||||
withNtfServer t = withNtfServerOn t ntfTestPort
|
||||
|
||||
runNtfTest :: forall c a. Transport c => (THandleNTF c -> IO a) -> IO a
|
||||
runNtfTest :: forall c a. Transport c => (THandleNTF c 'TClient -> IO a) -> IO a
|
||||
runNtfTest test = withNtfServer (transport @c) $ testNtfClient test
|
||||
|
||||
ntfServerTest ::
|
||||
@@ -151,7 +148,7 @@ ntfServerTest ::
|
||||
IO (Maybe TransmissionAuth, ByteString, ByteString, NtfResponse)
|
||||
ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h
|
||||
where
|
||||
tPut' :: THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO ()
|
||||
tPut' :: THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO ()
|
||||
tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do
|
||||
let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp)
|
||||
[Right ()] <- tPut h [Right (sig, t')]
|
||||
@@ -160,7 +157,7 @@ ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h
|
||||
[(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h
|
||||
pure (Nothing, corrId, qId, cmd)
|
||||
|
||||
ntfTest :: Transport c => TProxy c -> (THandleNTF c -> IO ()) -> Expectation
|
||||
ntfTest :: Transport c => TProxy c -> (THandleNTF c 'TClient -> IO ()) -> Expectation
|
||||
ntfTest _ test' = runNtfTest test' `shouldReturn` ()
|
||||
|
||||
data APNSMockRequest = APNSMockRequest
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
|
||||
|
||||
module NtfServerTests where
|
||||
|
||||
@@ -72,13 +72,13 @@ pattern RespNtf corrId queueId command <- (_, _, (corrId, queueId, Right command
|
||||
|
||||
deriving instance Eq NtfResponse
|
||||
|
||||
sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
sendRecvNtf h@THandle {params} (sgn, corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (sgn, tToSend)
|
||||
tGet1 h
|
||||
|
||||
signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
signSendRecvNtf h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (authorize tForAuth, tToSend)
|
||||
|
||||
+9
-12
@@ -12,7 +12,6 @@ module SMPAgentClient where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
@@ -202,7 +201,7 @@ initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer
|
||||
agentCfg :: AgentConfig
|
||||
agentCfg =
|
||||
defaultAgentConfig
|
||||
{ tcpPort = agentTestPort,
|
||||
{ tcpPort = Just agentTestPort,
|
||||
tbqSize = 4,
|
||||
-- database = testDB,
|
||||
smpCfg = defaultSMPClientConfig {qSize = 1, defaultTransport = (testPort, transport @TLS), networkConfig},
|
||||
@@ -224,11 +223,9 @@ fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000}
|
||||
fastMessageRetryInterval :: RetryInterval2
|
||||
fastMessageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
|
||||
|
||||
type AgentTestMonad m = (MonadUnliftIO m, MonadRandom m, MonadFail m)
|
||||
|
||||
withSmpAgentThreadOn_ :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> Int -> m () -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn_ :: ATransport -> (ServiceName, ServiceName, FilePath) -> Int -> IO () -> (ThreadId -> IO a) -> IO a
|
||||
withSmpAgentThreadOn_ t (port', smpPort', db') initClientId afterProcess =
|
||||
let cfg' = agentCfg {tcpPort = port'}
|
||||
let cfg' = agentCfg {tcpPort = Just port'}
|
||||
initServers' = initAgentServers {smp = userServers [ProtoServerWithAuth (SMPServer "localhost" smpPort' testKeyHash) Nothing]}
|
||||
in serverBracket
|
||||
( \started -> do
|
||||
@@ -241,24 +238,24 @@ withSmpAgentThreadOn_ t (port', smpPort', db') initClientId afterProcess =
|
||||
userServers :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ProtoServerWithAuth p))
|
||||
userServers srvs = M.fromList [(1, srvs)]
|
||||
|
||||
withSmpAgentThreadOn :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn :: ATransport -> (ServiceName, ServiceName, FilePath) -> (ThreadId -> IO a) -> IO a
|
||||
withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a 0 $ removeFile db'
|
||||
|
||||
withSmpAgentOn :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> m a -> m a
|
||||
withSmpAgentOn :: ATransport -> (ServiceName, ServiceName, FilePath) -> IO a -> IO a
|
||||
withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const
|
||||
|
||||
withSmpAgent :: AgentTestMonad m => ATransport -> m a -> m a
|
||||
withSmpAgent :: ATransport -> IO a -> IO a
|
||||
withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB)
|
||||
|
||||
testSMPAgentClientOn :: (Transport c, MonadUnliftIO m, MonadFail m) => ServiceName -> (c -> m a) -> m a
|
||||
testSMPAgentClientOn :: Transport c => ServiceName -> (c -> IO a) -> IO a
|
||||
testSMPAgentClientOn port' client = do
|
||||
Right useHost <- pure $ chooseTransportHost defaultNetworkConfig agentTestHost
|
||||
runTransportClient defaultTransportClientConfig Nothing useHost port' (Just testKeyHash) $ \h -> do
|
||||
line <- liftIO $ getLn h
|
||||
line <- getLn h
|
||||
if line == "Welcome to SMP agent v" <> B.pack simplexMQVersion
|
||||
then client h
|
||||
else do
|
||||
error $ "wrong welcome message: " <> B.unpack line
|
||||
|
||||
testSMPAgentClient :: (Transport c, MonadUnliftIO m, MonadFail m) => (c -> m a) -> m a
|
||||
testSMPAgentClient :: Transport c => (c -> IO a) -> IO a
|
||||
testSMPAgentClient = testSMPAgentClientOn agentTestPort
|
||||
|
||||
+40
-26
@@ -13,11 +13,11 @@
|
||||
module SMPClient where
|
||||
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Control.Monad.IO.Unlift
|
||||
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
|
||||
@@ -68,23 +68,25 @@ xit'' d t = do
|
||||
ci <- runIO $ lookupEnv "CI"
|
||||
(if ci == Just "true" then skip "skipped on CI" . it d else it d) t
|
||||
|
||||
testSMPClient :: (Transport c, MonadUnliftIO m, MonadFail m) => (THandleSMP c -> m a) -> m a
|
||||
testSMPClient :: Transport c => (THandleSMP c 'TClient -> IO a) -> IO a
|
||||
testSMPClient = testSMPClientVR supportedClientSMPRelayVRange
|
||||
|
||||
testSMPClientVR :: (Transport c, MonadUnliftIO m, MonadFail m) => VersionRangeSMP -> (THandleSMP c -> m a) -> m a
|
||||
testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a
|
||||
testSMPClientVR vr client = do
|
||||
Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost
|
||||
runTransportClient defaultTransportClientConfig Nothing useHost testPort (Just testKeyHash) $ \h -> do
|
||||
g <- liftIO C.newRandom
|
||||
ks <- atomically $ C.generateKeyPair g
|
||||
liftIO (runExceptT $ smpClientHandshake h ks testKeyHash vr) >>= \case
|
||||
testSMPClient_ useHost testPort vr client
|
||||
|
||||
testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a
|
||||
testSMPClient_ host port vr client = do
|
||||
runTransportClient defaultTransportClientConfig Nothing host port (Just testKeyHash) $ \h ->
|
||||
runExceptT (smpClientHandshake h Nothing testKeyHash vr) >>= \case
|
||||
Right th -> client th
|
||||
Left e -> error $ show e
|
||||
|
||||
cfg :: ServerConfig
|
||||
cfg =
|
||||
ServerConfig
|
||||
{ transports = undefined,
|
||||
{ transports = [],
|
||||
smpHandshakeTimeout = 60000000,
|
||||
tbqSize = 1,
|
||||
-- serverTbqSize = 1,
|
||||
@@ -95,6 +97,8 @@ cfg =
|
||||
storeMsgsFile = Nothing,
|
||||
allowNewQueues = True,
|
||||
newQueueBasicAuth = Nothing,
|
||||
controlPortUserAuth = Nothing,
|
||||
controlPortAdminAuth = Nothing,
|
||||
messageExpiration = Just defaultMessageExpiration,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
logStatsInterval = Nothing,
|
||||
@@ -106,12 +110,22 @@ cfg =
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
controlPort = Nothing
|
||||
controlPort = Nothing,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
allowSMPProxy = False
|
||||
}
|
||||
|
||||
cfgV7 :: ServerConfig
|
||||
cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}
|
||||
|
||||
proxyCfg :: ServerConfig
|
||||
proxyCfg =
|
||||
cfgV7
|
||||
{ allowSMPProxy = True,
|
||||
smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, agreeSecret = True}}
|
||||
}
|
||||
|
||||
withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile}
|
||||
|
||||
@@ -122,12 +136,12 @@ withSmpServerConfigOn :: HasCallStack => ATransport -> ServerConfig -> ServiceNa
|
||||
withSmpServerConfigOn t cfg' port' =
|
||||
serverBracket
|
||||
(\started -> runSMPServerBlocking started cfg' {transports = [(port', t)]})
|
||||
(pure ())
|
||||
(threadDelay 10000)
|
||||
|
||||
withSmpServerThreadOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withSmpServerThreadOn t = withSmpServerConfigOn t cfg
|
||||
|
||||
serverBracket :: (HasCallStack, MonadUnliftIO m) => (TMVar Bool -> m ()) -> m () -> (HasCallStack => ThreadId -> m a) -> m a
|
||||
serverBracket :: HasCallStack => (TMVar Bool -> IO ()) -> IO () -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
serverBracket process afterProcess f = do
|
||||
started <- newEmptyTMVarIO
|
||||
E.bracket
|
||||
@@ -149,16 +163,16 @@ withSmpServer t = withSmpServerOn t testPort
|
||||
withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a
|
||||
withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const
|
||||
|
||||
runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c -> IO a) -> IO a
|
||||
runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a
|
||||
runSmpTest test = withSmpServer (transport @c) $ testSMPClient test
|
||||
|
||||
runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a
|
||||
runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a
|
||||
runSmpTestN = runSmpTestNCfg cfg supportedClientSMPRelayVRange
|
||||
|
||||
runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a
|
||||
runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a
|
||||
runSmpTestNCfg srvCfg clntVR nClients test = withSmpServerConfigOn (transport @c) srvCfg testPort $ \_ -> run nClients []
|
||||
where
|
||||
run :: Int -> [THandleSMP c] -> IO a
|
||||
run :: Int -> [THandleSMP c 'TClient] -> IO a
|
||||
run 0 hs = test hs
|
||||
run n hs = testSMPClientVR clntVR $ \h -> run (n - 1) (h : hs)
|
||||
|
||||
@@ -170,7 +184,7 @@ smpServerTest ::
|
||||
IO (Maybe TransmissionAuth, ByteString, ByteString, BrokerMsg)
|
||||
smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h
|
||||
where
|
||||
tPut' :: THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO ()
|
||||
tPut' :: THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO ()
|
||||
tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do
|
||||
let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp)
|
||||
[Right ()] <- tPut h [Right (sig, t')]
|
||||
@@ -179,33 +193,33 @@ smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h
|
||||
[(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h
|
||||
pure (Nothing, corrId, qId, cmd)
|
||||
|
||||
smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> IO ()) -> Expectation
|
||||
smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
smpTest _ test' = runSmpTest test' `shouldReturn` ()
|
||||
|
||||
smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO ()) -> Expectation
|
||||
smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO ()) -> Expectation
|
||||
smpTestN n test' = runSmpTestN n test' `shouldReturn` ()
|
||||
|
||||
smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation
|
||||
smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
smpTest2 = smpTest2Cfg cfg supportedClientSMPRelayVRange
|
||||
|
||||
smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation
|
||||
smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
smpTest2Cfg srvCfg clntVR _ test' = runSmpTestNCfg srvCfg clntVR 2 _test `shouldReturn` ()
|
||||
where
|
||||
_test :: HasCallStack => [THandleSMP c] -> IO ()
|
||||
_test :: HasCallStack => [THandleSMP c 'TClient] -> IO ()
|
||||
_test [h1, h2] = test' h1 h2
|
||||
_test _ = error "expected 2 handles"
|
||||
|
||||
smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation
|
||||
smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
smpTest3 _ test' = smpTestN 3 _test
|
||||
where
|
||||
_test :: HasCallStack => [THandleSMP c] -> IO ()
|
||||
_test :: HasCallStack => [THandleSMP c 'TClient] -> IO ()
|
||||
_test [h1, h2, h3] = test' h1 h2 h3
|
||||
_test _ = error "expected 3 handles"
|
||||
|
||||
smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation
|
||||
smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
smpTest4 _ test' = smpTestN 4 _test
|
||||
where
|
||||
_test :: HasCallStack => [THandleSMP c] -> IO ()
|
||||
_test :: HasCallStack => [THandleSMP c 'TClient] -> IO ()
|
||||
_test [h1, h2, h3, h4] = test' h1 h2 h3 h4
|
||||
_test _ = error "expected 4 handles"
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module SMPProxyTests where
|
||||
|
||||
import AgentTests.FunctionalAPITests (runRight_)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import SMPAgentClient (testSMPServer, testSMPServer2)
|
||||
import SMPClient
|
||||
import qualified SMPClient as SMP
|
||||
import ServerTests (decryptMsgV3, sendRecv)
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
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 "forwarding requests" $ do
|
||||
describe "deliver message via SMP proxy" $ do
|
||||
it "same server" $
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do
|
||||
let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
let relayServ = proxyServ
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2"
|
||||
it "different servers" $
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ ->
|
||||
withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do
|
||||
let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2"
|
||||
xit "max message size, Ed448 keys" $
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ ->
|
||||
withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do
|
||||
g <- C.newRandom
|
||||
msg <- atomically $ C.randomBytes maxMessageLength g
|
||||
msg' <- atomically $ C.randomBytes maxMessageLength g
|
||||
let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd448 msg msg'
|
||||
it "max message size, Ed25519 keys" $
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ ->
|
||||
withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do
|
||||
g <- C.newRandom
|
||||
msg <- atomically $ C.randomBytes maxMessageLength g
|
||||
msg' <- atomically $ C.randomBytes maxMessageLength g
|
||||
let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash
|
||||
deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg msg'
|
||||
it "max message size, X25519 keys" $
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ ->
|
||||
withSmpServerConfigOn (transport @TLS) cfgV7 testPort2 $ \_ -> do
|
||||
g <- C.newRandom
|
||||
msg <- atomically $ C.randomBytes maxMessageLength g
|
||||
msg' <- atomically $ C.randomBytes maxMessageLength g
|
||||
let proxyServ = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
let relayServ = SMPServer SMP.testHost SMP.testPort2 SMP.testKeyHash
|
||||
deliverMessageViaProxy proxyServ relayServ C.SX25519 msg msg'
|
||||
xit "sender-proxy-relay-recipient works" todo
|
||||
xit "similar timing for proxied and direct sends" todo
|
||||
|
||||
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
|
||||
(sessId, v, relayKey) <- createSMPProxySession pc relayServ (Just "correct")
|
||||
-- send via proxy to unsecured queue
|
||||
proxySMPMessage pc sessId v relayKey Nothing sndId noMsgFlags msg
|
||||
-- receive 1
|
||||
(_tSess, _v, _sid, _ety, 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
|
||||
proxySMPMessage pc sessId v relayKey (Just sPriv) sndId noMsgFlags msg'
|
||||
-- receive 2
|
||||
(_tSess, _v, _sid, _ety, MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'}) <- atomically $ readTBQueue msgQ
|
||||
liftIO $ dec msgId' encBody' `shouldBe` Right msg'
|
||||
ackSMPMessage rc rPriv rcvId msgId'
|
||||
|
||||
proxyVRange :: VersionRangeSMP
|
||||
proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion
|
||||
|
||||
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 (ERR 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 (ERR AUTH)
|
||||
where
|
||||
proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"}
|
||||
|
||||
todo :: IO ()
|
||||
todo = do
|
||||
fail "TODO"
|
||||
+19
-20
@@ -78,13 +78,13 @@ pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh)
|
||||
pattern Msg :: MsgId -> MsgBody -> BrokerMsg
|
||||
pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body}
|
||||
|
||||
sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
sendRecv h@THandle {params} (sgn, corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (sgn, tToSend)
|
||||
tGet1 h
|
||||
|
||||
signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (authorize tForAuth, tToSend)
|
||||
@@ -93,17 +93,17 @@ signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do
|
||||
authorize t = case a of
|
||||
C.SEd25519 -> Just . TASignature . C.ASignature C.SEd25519 $ C.sign' pk t
|
||||
C.SEd448 -> Just . TASignature . C.ASignature C.SEd448 $ C.sign' pk t
|
||||
C.SX25519 -> (\THandleAuth {peerPubKey} -> TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t) <$> thAuth params
|
||||
C.SX25519 -> (\THAuthClient {serverPeerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t) <$> thAuth params
|
||||
#if !MIN_VERSION_base(4,18,0)
|
||||
_sx448 -> undefined -- ghc8107 fails to the branch excluded by types
|
||||
#endif
|
||||
|
||||
tPut1 :: Transport c => THandle v c -> SentRawTransmission -> IO (Either TransportError ())
|
||||
tPut1 :: Transport c => THandle v c 'TClient -> SentRawTransmission -> IO (Either TransportError ())
|
||||
tPut1 h t = do
|
||||
[r] <- tPut h [Right t]
|
||||
pure r
|
||||
|
||||
tGet1 :: (ProtocolEncoding v err cmd, Transport c, MonadIO m, MonadFail m) => THandle v c -> m (SignedTransmission err cmd)
|
||||
tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c 'TClient -> IO (SignedTransmission err cmd)
|
||||
tGet1 h = do
|
||||
[r] <- liftIO $ tGet h
|
||||
pure r
|
||||
@@ -555,12 +555,12 @@ testWithStoreLog at@(ATransport t) =
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
removeFile testStoreLogFile
|
||||
where
|
||||
runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation
|
||||
runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation
|
||||
runTest _ test' server = do
|
||||
testSMPClient test' `shouldReturn` ()
|
||||
killThread server
|
||||
|
||||
runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation
|
||||
runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
runClient _ test' = testSMPClient test' `shouldReturn` ()
|
||||
|
||||
logSize :: FilePath -> IO Int
|
||||
@@ -653,12 +653,12 @@ testRestoreMessages at@(ATransport t) =
|
||||
removeFile testStoreMsgsFile
|
||||
removeFile testServerStatsBackupFile
|
||||
where
|
||||
runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation
|
||||
runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation
|
||||
runTest _ test' server = do
|
||||
testSMPClient test' `shouldReturn` ()
|
||||
killThread server
|
||||
|
||||
runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation
|
||||
runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
runClient _ test' = testSMPClient test' `shouldReturn` ()
|
||||
|
||||
checkStats :: ServerStatsData -> [RecipientId] -> Int -> Int -> Expectation
|
||||
@@ -727,15 +727,15 @@ testRestoreExpireMessages at@(ATransport t) =
|
||||
Right ServerStatsData {_msgExpired} <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
_msgExpired `shouldBe` 2
|
||||
where
|
||||
runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation
|
||||
runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation
|
||||
runTest _ test' server = do
|
||||
testSMPClient test' `shouldReturn` ()
|
||||
killThread server
|
||||
|
||||
runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation
|
||||
runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
runClient _ test' = testSMPClient test' `shouldReturn` ()
|
||||
|
||||
createAndSecureQueue :: Transport c => THandleSMP c -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret)
|
||||
createAndSecureQueue :: Transport c => THandleSMP c 'TClient -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret)
|
||||
createAndSecureQueue h sPub = do
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
@@ -759,8 +759,8 @@ testTiming (ATransport t) =
|
||||
timingTests :: [(C.AuthAlg, C.AuthAlg, Int)]
|
||||
timingTests =
|
||||
[ (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd25519, 200), -- correct key type
|
||||
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150),
|
||||
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200),
|
||||
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150),
|
||||
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200),
|
||||
(C.AuthAlg C.SEd448, C.AuthAlg C.SEd25519, 200),
|
||||
(C.AuthAlg C.SEd448, C.AuthAlg C.SEd448, 150), -- correct key type
|
||||
(C.AuthAlg C.SEd448, C.AuthAlg C.SX25519, 200),
|
||||
@@ -769,8 +769,8 @@ testTiming (ATransport t) =
|
||||
(C.AuthAlg C.SX25519, C.AuthAlg C.SX25519, 200) -- correct key type
|
||||
]
|
||||
timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const
|
||||
similarTime t1 t2 = abs (t2 / t1 - 1) < 0.15 -- normally the difference between "no queue" and "wrong key" is less than 5%
|
||||
testSameTiming :: forall c. Transport c => THandleSMP c -> THandleSMP c -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation
|
||||
similarTime t1 t2 = abs (t2 / t1 - 1) < 0.2 -- normally the difference between "no queue" and "wrong key" is less than 5%
|
||||
testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation
|
||||
testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, n) = do
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g
|
||||
@@ -791,10 +791,11 @@ testTiming (ATransport t) =
|
||||
|
||||
runTimingTest sh badKey sId $ _SEND "hello"
|
||||
where
|
||||
runTimingTest :: PartyI p => THandleSMP c -> C.APrivateAuthKey -> ByteString -> Command p -> IO ()
|
||||
runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> ByteString -> Command p -> IO ()
|
||||
runTimingTest h badKey qId cmd = do
|
||||
threadDelay 100000
|
||||
_ <- timeRepeat n $ do -- "warm up" the server
|
||||
_ <- timeRepeat n $ do
|
||||
-- "warm up" the server
|
||||
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd)
|
||||
return ()
|
||||
threadDelay 100000
|
||||
@@ -930,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)
|
||||
|
||||
+3
-1
@@ -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)
|
||||
@@ -46,7 +47,7 @@ main = do
|
||||
$ do
|
||||
describe "Agent SQLite schema dump" schemaDumpTest
|
||||
describe "Core tests" $ do
|
||||
describe "Batching tests" batchingTests
|
||||
xdescribe "Batching tests" batchingTests
|
||||
describe "Encoding tests" encodingTests
|
||||
describe "Protocol error tests" protocolErrorTests
|
||||
describe "Version range" versionRangeTests
|
||||
@@ -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
|
||||
|
||||
+290
-275
@@ -8,7 +8,7 @@
|
||||
|
||||
module XFTPAgent where
|
||||
|
||||
import AgentTests.FunctionalAPITests (get, getSMPAgentClient', rfGet, runRight, runRight_, sfGet)
|
||||
import AgentTests.FunctionalAPITests (get, rfGet, runRight, runRight_, sfGet, withAgent)
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
@@ -20,12 +20,14 @@ import Data.Int (Int64)
|
||||
import Data.List (find, isSuffixOf)
|
||||
import Data.Maybe (fromJust)
|
||||
import SMPAgentClient (agentCfg, initAgentServers, testDB, testDB2, testDB3)
|
||||
import Simplex.FileTransfer.Description (FileDescription (..), FileDescriptionURI (..), ValidFileDescription, fileDescriptionURI, mb, qrSizeLimit, pattern ValidFileDescription)
|
||||
import Simplex.FileTransfer.Client (XFTPClientConfig (..))
|
||||
import Simplex.FileTransfer.Description (FileChunk (..), FileDescription (..), FileDescriptionURI (..), ValidFileDescription, fileDescriptionURI, kb, mb, qrSizeLimit, pattern ValidFileDescription)
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (AUTH))
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
|
||||
import Simplex.Messaging.Agent (AgentClient, disconnectAgentClient, testProtocolServer, xftpDeleteRcvFile, xftpDeleteSndFileInternal, xftpDeleteSndFileRemote, xftpReceiveFile, xftpSendDescription, xftpSendFile, xftpStartWorkers)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (AUTH))
|
||||
import Simplex.Messaging.Agent (AgentClient, testProtocolServer, xftpDeleteRcvFile, xftpDeleteSndFileInternal, xftpDeleteSndFileRemote, xftpReceiveFile, xftpSendDescription, xftpSendFile, xftpStartWorkers)
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, xftpCfg)
|
||||
import Simplex.Messaging.Agent.Protocol (ACommand (..), AgentErrorType (..), BrokerErrorType (..), RcvFileId, SndFileId, noAuthSrv)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs)
|
||||
@@ -48,6 +50,7 @@ xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do
|
||||
it "should send and receive with encrypted local files" testXFTPAgentSendReceiveEncrypted
|
||||
it "should send and receive large file with a redirect" testXFTPAgentSendReceiveRedirect
|
||||
it "should send and receive small file without a redirect" testXFTPAgentSendReceiveNoRedirect
|
||||
describe "sending and receiving with version negotiation" testXFTPAgentSendReceiveMatrix
|
||||
it "should resume receiving file after restart" testXFTPAgentReceiveRestore
|
||||
it "should cleanup rcv tmp path after permanent error" testXFTPAgentReceiveCleanup
|
||||
it "should resume sending file after restart" testXFTPAgentSendRestore
|
||||
@@ -90,7 +93,7 @@ sfProgress c expected = loop 0
|
||||
-- checks that progress increases till it reaches total
|
||||
checkProgress :: (HasCallStack, MonadIO m) => (Int64, Int64) -> (Int64, Int64) -> (Int64 -> m ()) -> m ()
|
||||
checkProgress (prev, expected) (progress, total) loop
|
||||
| total /= expected = error "total /= expected"
|
||||
| total /= expected = liftIO (print total) >> error "total /= expected"
|
||||
| progress <= prev = error "progress <= prev"
|
||||
| progress > total = error "progress > total"
|
||||
| progress < total = loop progress
|
||||
@@ -100,22 +103,18 @@ testXFTPAgentSendReceive :: HasCallStack => IO ()
|
||||
testXFTPAgentSendReceive = withXFTPServer $ do
|
||||
filePath <- createRandomFile
|
||||
-- send file, delete snd file internally
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
(rfd1, rfd2) <- runRight $ do
|
||||
(rfd1, rfd2) <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
(sfId, _, rfd1, rfd2) <- testSend sndr filePath
|
||||
xftpDeleteSndFileInternal sndr sfId
|
||||
liftIO $ xftpDeleteSndFileInternal sndr sfId
|
||||
pure (rfd1, rfd2)
|
||||
|
||||
-- receive file, delete rcv file
|
||||
testReceiveDelete 2 rfd1 filePath
|
||||
testReceiveDelete 3 rfd2 filePath
|
||||
where
|
||||
testReceiveDelete clientId rfd originalFilePath = do
|
||||
rcp <- getSMPAgentClient' clientId agentCfg initAgentServers testDB2
|
||||
runRight_ $ do
|
||||
rfId <- testReceive rcp rfd originalFilePath
|
||||
testReceiveDelete clientId rfd originalFilePath =
|
||||
withAgent clientId agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
rfId <- runRight $ testReceive rcp rfd originalFilePath
|
||||
xftpDeleteRcvFile rcp rfId
|
||||
disconnectAgentClient rcp
|
||||
|
||||
testXFTPAgentSendReceiveEncrypted :: HasCallStack => IO ()
|
||||
testXFTPAgentSendReceiveEncrypted = withXFTPServer $ do
|
||||
@@ -124,22 +123,19 @@ testXFTPAgentSendReceiveEncrypted = withXFTPServer $ do
|
||||
s <- LB.readFile filePath
|
||||
file <- atomically $ CryptoFile (senderFiles </> "encrypted_testfile") . Just <$> CF.randomArgs g
|
||||
runRight_ $ CF.writeFile file s
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
(rfd1, rfd2) <- runRight $ do
|
||||
(rfd1, rfd2) <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
(sfId, _, rfd1, rfd2) <- testSendCF sndr file
|
||||
xftpDeleteSndFileInternal sndr sfId
|
||||
liftIO $ xftpDeleteSndFileInternal sndr sfId
|
||||
pure (rfd1, rfd2)
|
||||
-- receive file, delete rcv file
|
||||
testReceiveDelete 2 rfd1 filePath g
|
||||
testReceiveDelete 3 rfd2 filePath g
|
||||
where
|
||||
testReceiveDelete clientId rfd originalFilePath g = do
|
||||
rcp <- getSMPAgentClient' clientId agentCfg initAgentServers testDB2
|
||||
cfArgs <- atomically $ Just <$> CF.randomArgs g
|
||||
runRight_ $ do
|
||||
rfId <- testReceiveCF rcp rfd cfArgs originalFilePath
|
||||
testReceiveDelete clientId rfd originalFilePath g =
|
||||
withAgent clientId agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
cfArgs <- atomically $ Just <$> CF.randomArgs g
|
||||
rfId <- runRight $ testReceiveCF rcp rfd cfArgs originalFilePath
|
||||
xftpDeleteRcvFile rcp rfId
|
||||
disconnectAgentClient rcp
|
||||
|
||||
testXFTPAgentSendReceiveRedirect :: HasCallStack => IO ()
|
||||
testXFTPAgentSendReceiveRedirect = withXFTPServer $ do
|
||||
@@ -147,93 +143,126 @@ testXFTPAgentSendReceiveRedirect = withXFTPServer $ do
|
||||
filePathIn <- createRandomFile
|
||||
let fileSize = mb 17
|
||||
totalSize = fileSize + mb 1
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
directFileId <- runRight $ xftpSendFile sndr 1 (CryptoFile filePathIn Nothing) 1
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 4194304 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 8388608 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 12582912 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 16777216 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 17825792 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG totalSize totalSize)
|
||||
vfdDirect <-
|
||||
sfGet sndr >>= \case
|
||||
(_, _, SFDONE _snd (vfd : _)) -> pure vfd
|
||||
r -> error $ "Expected SFDONE, got " <> show r
|
||||
redirectFileId <- runRight $ xftpSendDescription sndr 1 vfdDirect 1
|
||||
logInfo $ "File sent, sending redirect: " <> tshow redirectFileId
|
||||
sfGet sndr `shouldReturn` ("", redirectFileId, SFPROG 65536 65536)
|
||||
vfdRedirect@(ValidFileDescription fdRedirect) <-
|
||||
sfGet sndr >>= \case
|
||||
(_, _, SFDONE _snd (vfd : _)) -> pure vfd
|
||||
r -> error $ "Expected SFDONE, got " <> show r
|
||||
case fdRedirect of
|
||||
FileDescription {redirect = Just _} -> pure ()
|
||||
_ -> error "missing RedirectFileInfo"
|
||||
let uri = strEncode $ fileDescriptionURI vfdRedirect
|
||||
case strDecode uri of
|
||||
Left err -> fail err
|
||||
Right ok -> ok `shouldBe` fileDescriptionURI vfdRedirect
|
||||
disconnectAgentClient sndr
|
||||
--- recipient
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
FileDescriptionURI {description} <- either fail pure $ strDecode uri
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
directFileId <- runRight $ xftpSendFile sndr 1 (CryptoFile filePathIn Nothing) 1
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 4194304 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 8388608 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 12582912 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 16777216 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 17825792 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG totalSize totalSize)
|
||||
vfdDirect <-
|
||||
sfGet sndr >>= \case
|
||||
(_, _, SFDONE _snd (vfd : _)) -> pure vfd
|
||||
r -> error $ "Expected SFDONE, got " <> show r
|
||||
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing
|
||||
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)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 12582912 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 16777216 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 17825792 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG totalSize totalSize)
|
||||
out <-
|
||||
rfGet rcp >>= \case
|
||||
(_, _, RFDONE out) -> pure out
|
||||
r -> error $ "Expected RFDONE, got " <> show r
|
||||
disconnectAgentClient rcp
|
||||
testNoRedundancy vfdDirect
|
||||
|
||||
inBytes <- B.readFile filePathIn
|
||||
B.readFile out `shouldReturn` inBytes
|
||||
redirectFileId <- runRight $ xftpSendDescription sndr 1 vfdDirect 1
|
||||
logInfo $ "File sent, sending redirect: " <> tshow redirectFileId
|
||||
sfGet sndr `shouldReturn` ("", redirectFileId, SFPROG 65536 65536)
|
||||
vfdRedirect@(ValidFileDescription fdRedirect) <-
|
||||
sfGet sndr >>= \case
|
||||
(_, _, SFDONE _snd (vfd : _)) -> pure vfd
|
||||
r -> error $ "Expected SFDONE, got " <> show r
|
||||
|
||||
testNoRedundancy vfdRedirect
|
||||
|
||||
case fdRedirect of
|
||||
FileDescription {redirect = Just _} -> pure ()
|
||||
_ -> error "missing RedirectFileInfo"
|
||||
let uri = strEncode $ fileDescriptionURI vfdRedirect
|
||||
case strDecode uri of
|
||||
Left err -> fail err
|
||||
Right ok -> ok `shouldBe` fileDescriptionURI vfdRedirect
|
||||
--- recipient
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
FileDescriptionURI {description} <- either fail pure $ strDecode uri
|
||||
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing
|
||||
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)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 12582912 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 16777216 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 17825792 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG totalSize totalSize)
|
||||
out <-
|
||||
rfGet rcp >>= \case
|
||||
(_, _, RFDONE out) -> pure out
|
||||
r -> error $ "Expected RFDONE, got " <> show r
|
||||
|
||||
inBytes <- B.readFile filePathIn
|
||||
B.readFile out `shouldReturn` inBytes
|
||||
|
||||
testXFTPAgentSendReceiveNoRedirect :: HasCallStack => IO ()
|
||||
testXFTPAgentSendReceiveNoRedirect = withXFTPServer $ do
|
||||
--- sender
|
||||
let fileSize = mb 5
|
||||
filePathIn <- createRandomFile_ fileSize "testfile"
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
directFileId <- runRight $ xftpSendFile sndr 1 (CryptoFile filePathIn Nothing) 1
|
||||
let totalSize = fileSize + mb 1
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 4194304 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 5242880 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG totalSize totalSize)
|
||||
vfdDirect <-
|
||||
sfGet sndr >>= \case
|
||||
(_, _, SFDONE _snd (vfd : _)) -> pure vfd
|
||||
r -> error $ "Expected SFDONE, got " <> show r
|
||||
let uri = strEncode $ fileDescriptionURI vfdDirect
|
||||
B.length uri `shouldSatisfy` (< qrSizeLimit)
|
||||
case strDecode uri of
|
||||
Left err -> fail err
|
||||
Right ok -> ok `shouldBe` fileDescriptionURI vfdDirect
|
||||
disconnectAgentClient sndr
|
||||
--- recipient
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
FileDescriptionURI {description} <- either fail pure $ strDecode uri
|
||||
let ValidFileDescription FileDescription {redirect} = description
|
||||
redirect `shouldBe` Nothing
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing
|
||||
-- NO extra "RFPROG 65k 65k" before switching to real file
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 5242880 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG totalSize totalSize)
|
||||
out <-
|
||||
rfGet rcp >>= \case
|
||||
(_, _, RFDONE out) -> pure out
|
||||
r -> error $ "Expected RFDONE, got " <> show r
|
||||
disconnectAgentClient rcp
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
directFileId <- runRight $ xftpSendFile sndr 1 (CryptoFile filePathIn Nothing) 1
|
||||
let totalSize = fileSize + mb 1
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 4194304 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG 5242880 totalSize)
|
||||
sfGet sndr `shouldReturn` ("", directFileId, SFPROG totalSize totalSize)
|
||||
vfdDirect <-
|
||||
sfGet sndr >>= \case
|
||||
(_, _, SFDONE _snd (vfd : _)) -> pure vfd
|
||||
r -> error $ "Expected SFDONE, got " <> show r
|
||||
|
||||
inBytes <- B.readFile filePathIn
|
||||
B.readFile out `shouldReturn` inBytes
|
||||
testNoRedundancy vfdDirect
|
||||
|
||||
let uri = strEncode $ fileDescriptionURI vfdDirect
|
||||
B.length uri `shouldSatisfy` (< qrSizeLimit)
|
||||
case strDecode uri of
|
||||
Left err -> fail err
|
||||
Right ok -> ok `shouldBe` fileDescriptionURI vfdDirect
|
||||
--- recipient
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
FileDescriptionURI {description} <- either fail pure $ strDecode uri
|
||||
let ValidFileDescription FileDescription {redirect} = description
|
||||
redirect `shouldBe` Nothing
|
||||
rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing
|
||||
-- NO extra "RFPROG 65k 65k" before switching to real file
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 5242880 totalSize)
|
||||
rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG totalSize totalSize)
|
||||
out <-
|
||||
rfGet rcp >>= \case
|
||||
(_, _, RFDONE out) -> pure out
|
||||
r -> error $ "Expected RFDONE, got " <> show r
|
||||
|
||||
inBytes <- B.readFile filePathIn
|
||||
B.readFile out `shouldReturn` inBytes
|
||||
|
||||
testXFTPAgentSendReceiveMatrix :: Spec
|
||||
testXFTPAgentSendReceiveMatrix = do
|
||||
describe "old server" $ do
|
||||
it "new clients" $ run oldServer newClient newClient
|
||||
it "new sender, old recipient" $ run oldServer newClient newClient
|
||||
it "old sender, new recipient" $ run oldServer oldClient newClient
|
||||
it "old clients" $ run oldServer oldClient oldClient
|
||||
describe "new server" $ do
|
||||
it "new clients" $ run newServer newClient newClient
|
||||
it "new sender, old recipient" $ run newServer newClient newClient
|
||||
it "old sender, new recipient" $ run newServer oldClient newClient
|
||||
it "old clients" $ run newServer oldClient oldClient
|
||||
where
|
||||
oldClient = agentCfg {xftpCfg = (xftpCfg agentCfg) {clientALPN = Nothing}}
|
||||
newClient = agentCfg
|
||||
oldServer = testXFTPServerConfig_ Nothing
|
||||
newServer = testXFTPServerConfig
|
||||
run :: HasCallStack => XFTPServerConfig -> AgentConfig -> AgentConfig -> IO ()
|
||||
run server sender receiver =
|
||||
withXFTPServerCfg server $ \_t -> do
|
||||
filePath <- createRandomFile_ (kb 319 :: Integer) "testfile"
|
||||
rfd <- withAgent 1 sender initAgentServers testDB $ \sndr -> do
|
||||
(sfId, _, rfd1, _) <- runRight $ testSendCF' sndr (CF.plain filePath) (kb 320)
|
||||
rfd1 <$ xftpDeleteSndFileInternal sndr sfId
|
||||
withAgent 2 receiver initAgentServers testDB2 $ \rcp -> do
|
||||
rfId <- runRight $ testReceiveCF' rcp rfd Nothing filePath (kb 320)
|
||||
xftpDeleteRcvFile rcp rfId
|
||||
|
||||
createRandomFile :: HasCallStack => IO FilePath
|
||||
createRandomFile = createRandomFile' "testfile"
|
||||
@@ -252,29 +281,38 @@ testSend :: HasCallStack => AgentClient -> FilePath -> ExceptT AgentErrorType IO
|
||||
testSend sndr = testSendCF sndr . CF.plain
|
||||
|
||||
testSendCF :: HasCallStack => AgentClient -> CryptoFile -> ExceptT AgentErrorType IO (SndFileId, ValidFileDescription 'FSender, ValidFileDescription 'FRecipient, ValidFileDescription 'FRecipient)
|
||||
testSendCF sndr file = do
|
||||
testSendCF sndr file = testSendCF' sndr file $ mb 18
|
||||
|
||||
testSendCF' :: HasCallStack => AgentClient -> CryptoFile -> Int64 -> ExceptT AgentErrorType IO (SndFileId, ValidFileDescription 'FSender, ValidFileDescription 'FRecipient, ValidFileDescription 'FRecipient)
|
||||
testSendCF' sndr file size = do
|
||||
xftpStartWorkers sndr (Just senderFiles)
|
||||
sfId <- xftpSendFile sndr 1 file 2
|
||||
sfProgress sndr $ mb 18
|
||||
sfProgress sndr size
|
||||
("", sfId', SFDONE sndDescr [rfd1, rfd2]) <- sfGet sndr
|
||||
liftIO $ testNoRedundancy rfd1
|
||||
liftIO $ testNoRedundancy rfd2
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
pure (sfId, sndDescr, rfd1, rfd2)
|
||||
|
||||
testNoRedundancy :: HasCallStack => ValidFileDescription 'FRecipient -> IO ()
|
||||
testNoRedundancy (ValidFileDescription FileDescription {chunks}) =
|
||||
all (\FileChunk {replicas} -> length replicas == 1) chunks `shouldBe` True
|
||||
|
||||
testReceive :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> FilePath -> ExceptT AgentErrorType IO RcvFileId
|
||||
testReceive rcp rfd = testReceiveCF rcp rfd Nothing
|
||||
|
||||
testReceiveCF :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> ExceptT AgentErrorType IO RcvFileId
|
||||
testReceiveCF rcp rfd cfArgs originalFilePath = do
|
||||
xftpStartWorkers rcp (Just recipientFiles)
|
||||
testReceiveCF' rcp rfd cfArgs originalFilePath
|
||||
testReceiveCF' rcp rfd cfArgs originalFilePath $ mb 18
|
||||
|
||||
testReceive' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> FilePath -> ExceptT AgentErrorType IO RcvFileId
|
||||
testReceive' rcp rfd = testReceiveCF' rcp rfd Nothing
|
||||
testReceive' rcp rfd originalFilePath = testReceiveCF' rcp rfd Nothing originalFilePath $ mb 18
|
||||
|
||||
testReceiveCF' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> ExceptT AgentErrorType IO RcvFileId
|
||||
testReceiveCF' rcp rfd cfArgs originalFilePath = do
|
||||
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
|
||||
rfProgress rcp $ mb 18
|
||||
rfProgress rcp size
|
||||
("", rfId', RFDONE path) <- rfGet rcp
|
||||
liftIO $ do
|
||||
rfId' `shouldBe` rfId
|
||||
@@ -286,52 +324,48 @@ logCfgNoLogs :: LogConfig
|
||||
logCfgNoLogs = LogConfig {lc_file = Nothing, lc_stderr = False}
|
||||
|
||||
testXFTPAgentReceiveRestore :: HasCallStack => IO ()
|
||||
testXFTPAgentReceiveRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
testXFTPAgentReceiveRestore = do
|
||||
filePath <- createRandomFile
|
||||
|
||||
rfd <- withXFTPServerStoreLogOn $ \_ -> do
|
||||
rfd <- withXFTPServerStoreLogOn $ \_ ->
|
||||
-- send file
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
runRight $ do
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
(_, _, rfd, _) <- testSend sndr filePath
|
||||
pure rfd
|
||||
|
||||
-- receive file - should not succeed with server down
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
rfId <- runRight $ do
|
||||
rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do
|
||||
xftpStartWorkers rcp (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp 1 rfd Nothing
|
||||
liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt
|
||||
pure rfId
|
||||
disconnectAgentClient rcp
|
||||
|
||||
[prefixDir] <- listDirectory recipientFiles
|
||||
let tmpPath = recipientFiles </> prefixDir </> "xftp.encrypted"
|
||||
doesDirectoryExist tmpPath `shouldReturn` True
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
withXFTPServerStoreLogOn $ \_ ->
|
||||
-- receive file - should start downloading with server up
|
||||
rcp' <- getSMPAgentClient' 3 agentCfg initAgentServers testDB2
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
("", rfId', RFPROG _ _) <- rfGet rcp'
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
disconnectAgentClient rcp'
|
||||
|
||||
withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
("", rfId', RFPROG _ _) <- rfGet rcp'
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
threadDelay 100000
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
withXFTPServerStoreLogOn $ \_ ->
|
||||
-- receive file - should continue downloading with server up
|
||||
rcp' <- getSMPAgentClient' 4 agentCfg initAgentServers testDB2
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
rfProgress rcp' $ mb 18
|
||||
("", rfId', RFDONE path) <- rfGet rcp'
|
||||
liftIO $ do
|
||||
rfId' `shouldBe` rfId
|
||||
file <- B.readFile filePath
|
||||
B.readFile path `shouldReturn` file
|
||||
withAgent 4 agentCfg initAgentServers testDB2 $ \rcp' -> do
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
rfProgress rcp' $ mb 18
|
||||
("", rfId', RFDONE path) <- rfGet rcp'
|
||||
liftIO $ do
|
||||
rfId' `shouldBe` rfId
|
||||
file <- B.readFile filePath
|
||||
B.readFile path `shouldReturn` file
|
||||
|
||||
-- tmp path should be removed after receiving file
|
||||
doesDirectoryExist tmpPath `shouldReturn` False
|
||||
threadDelay 100000
|
||||
-- tmp path should be removed after receiving file
|
||||
doesDirectoryExist tmpPath `shouldReturn` False
|
||||
|
||||
testXFTPAgentReceiveCleanup :: HasCallStack => IO ()
|
||||
testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
@@ -339,30 +373,27 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
|
||||
rfd <- withXFTPServerStoreLogOn $ \_ -> do
|
||||
-- send file
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
runRight $ do
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
(_, _, rfd, _) <- testSend sndr filePath
|
||||
pure rfd
|
||||
|
||||
-- receive file - should not succeed with server down
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
rfId <- runRight $ do
|
||||
rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do
|
||||
xftpStartWorkers rcp (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp 1 rfd Nothing
|
||||
liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt
|
||||
pure rfId
|
||||
disconnectAgentClient rcp
|
||||
|
||||
[prefixDir] <- listDirectory recipientFiles
|
||||
let tmpPath = recipientFiles </> prefixDir </> "xftp.encrypted"
|
||||
doesDirectoryExist tmpPath `shouldReturn` True
|
||||
|
||||
withXFTPServerThreadOn $ \_ -> do
|
||||
withXFTPServerThreadOn $ \_ ->
|
||||
-- receive file - should fail with AUTH error
|
||||
rcp' <- getSMPAgentClient' 3 agentCfg initAgentServers testDB2
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp'
|
||||
rfId' `shouldBe` rfId
|
||||
withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do
|
||||
runRight_ $ xftpStartWorkers rcp' (Just recipientFiles)
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp'
|
||||
rfId' `shouldBe` rfId
|
||||
|
||||
-- tmp path should be removed after permanent error
|
||||
doesDirectoryExist tmpPath `shouldReturn` False
|
||||
@@ -372,13 +403,11 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
filePath <- createRandomFile
|
||||
|
||||
-- send file - should not succeed with server down
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
sfId <- runRight $ do
|
||||
sfId <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
xftpStartWorkers sndr (Just senderFiles)
|
||||
sfId <- xftpSendFile sndr 1 (CF.plain filePath) 2
|
||||
liftIO $ timeout 1000000 (get sndr) `shouldReturn` Nothing -- wait for worker to encrypt and attempt to create file
|
||||
pure sfId
|
||||
disconnectAgentClient sndr
|
||||
|
||||
dirEntries <- listDirectory senderFiles
|
||||
let prefixDir = fromJust $ find (isSuffixOf "_snd.xftp") dirEntries
|
||||
@@ -387,23 +416,25 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
doesDirectoryExist prefixPath `shouldReturn` True
|
||||
doesFileExist encPath `shouldReturn` True
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
withXFTPServerStoreLogOn $ \_ ->
|
||||
-- send file - should start uploading with server up
|
||||
sndr' <- getSMPAgentClient' 2 agentCfg initAgentServers testDB
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
("", sfId', SFPROG _ _) <- sfGet sndr'
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
disconnectAgentClient sndr'
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
("", sfId', SFPROG _ _) <- sfGet sndr'
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
|
||||
threadDelay 100000
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
-- send file - should continue uploading with server up
|
||||
sndr' <- getSMPAgentClient' 3 agentCfg initAgentServers testDB
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
sfProgress sndr' $ mb 18
|
||||
("", sfId', SFDONE _sndDescr [rfd1, _rfd2]) <- sfGet sndr'
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
rfd1 <- withAgent 3 agentCfg initAgentServers testDB $ \sndr' -> do
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
sfProgress sndr' $ mb 18
|
||||
("", sfId', SFDONE _sndDescr [rfd1, rfd2]) <- sfGet sndr'
|
||||
liftIO $ testNoRedundancy rfd1
|
||||
liftIO $ testNoRedundancy rfd2
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
pure rfd1
|
||||
|
||||
-- prefix path should be removed after sending file
|
||||
threadDelay 100000
|
||||
@@ -411,18 +442,16 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
doesFileExist encPath `shouldReturn` False
|
||||
|
||||
-- receive file
|
||||
rcp <- getSMPAgentClient' 4 agentCfg initAgentServers testDB2
|
||||
runRight_ . void $
|
||||
testReceive rcp rfd1 filePath
|
||||
withAgent 4 agentCfg initAgentServers testDB2 $ \rcp ->
|
||||
runRight_ . void $ testReceive rcp rfd1 filePath
|
||||
|
||||
testXFTPAgentSendCleanup :: HasCallStack => IO ()
|
||||
testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
filePath <- createRandomFile
|
||||
|
||||
sfId <- withXFTPServerStoreLogOn $ \_ -> do
|
||||
sfId <- withXFTPServerStoreLogOn $ \_ ->
|
||||
-- send file
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
sfId <- runRight $ do
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
xftpStartWorkers sndr (Just senderFiles)
|
||||
sfId <- xftpSendFile sndr 1 (CF.plain filePath) 2
|
||||
-- wait for progress events for 5 out of 6 chunks - at this point all chunks should be created on the server
|
||||
@@ -430,8 +459,6 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
(_, _, SFPROG _ _) <- sfGet sndr
|
||||
pure ()
|
||||
pure sfId
|
||||
disconnectAgentClient sndr
|
||||
pure sfId
|
||||
|
||||
dirEntries <- listDirectory senderFiles
|
||||
let prefixDir = fromJust $ find (isSuffixOf "_snd.xftp") dirEntries
|
||||
@@ -440,16 +467,16 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do
|
||||
doesDirectoryExist prefixPath `shouldReturn` True
|
||||
doesFileExist encPath `shouldReturn` True
|
||||
|
||||
withXFTPServerThreadOn $ \_ -> do
|
||||
withXFTPServerThreadOn $ \_ ->
|
||||
-- send file - should fail with AUTH error
|
||||
sndr' <- getSMPAgentClient' 2 agentCfg initAgentServers testDB
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
("", sfId', SFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- sfGet sndr'
|
||||
sfId' `shouldBe` sfId
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
("", sfId', SFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- sfGet sndr'
|
||||
sfId' `shouldBe` sfId
|
||||
|
||||
-- prefix path should be removed after permanent error
|
||||
doesDirectoryExist prefixPath `shouldReturn` False
|
||||
doesFileExist encPath `shouldReturn` False
|
||||
-- prefix path should be removed after permanent error
|
||||
doesDirectoryExist prefixPath `shouldReturn` False
|
||||
doesFileExist encPath `shouldReturn` False
|
||||
|
||||
testXFTPAgentDelete :: HasCallStack => IO ()
|
||||
testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $
|
||||
@@ -457,34 +484,30 @@ testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $
|
||||
filePath <- createRandomFile
|
||||
|
||||
-- send file
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
(sfId, sndDescr, rfd1, rfd2) <- runRight $ testSend sndr filePath
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
(sfId, sndDescr, rfd1, rfd2) <- runRight $ testSend sndr filePath
|
||||
|
||||
-- receive file
|
||||
rcp1 <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ . void $
|
||||
testReceive rcp1 rfd1 filePath
|
||||
-- receive file
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp1 -> do
|
||||
runRight_ . void $ testReceive rcp1 rfd1 filePath
|
||||
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
|
||||
-- delete file
|
||||
runRight $ do
|
||||
xftpStartWorkers sndr (Just senderFiles)
|
||||
xftpDeleteSndFileRemote sndr 1 sfId sndDescr
|
||||
Nothing <- liftIO $ 100000 `timeout` sfGet sndr
|
||||
pure ()
|
||||
disconnectAgentClient rcp1
|
||||
-- delete file
|
||||
runRight_ $ xftpStartWorkers sndr (Just senderFiles)
|
||||
xftpDeleteSndFileRemote sndr 1 sfId sndDescr
|
||||
Nothing <- 100000 `timeout` sfGet sndr
|
||||
pure ()
|
||||
|
||||
threadDelay 1000000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
threadDelay 1000000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
|
||||
-- receive file - should fail with AUTH error
|
||||
rcp2 <- getSMPAgentClient' 3 agentCfg initAgentServers testDB2
|
||||
runRight $ do
|
||||
xftpStartWorkers rcp2 (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
-- 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
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
|
||||
testXFTPAgentDeleteRestore :: HasCallStack => IO ()
|
||||
testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
@@ -492,43 +515,36 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
|
||||
(sfId, sndDescr, rfd2) <- withXFTPServerStoreLogOn $ \_ -> do
|
||||
-- send file
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
(sfId, sndDescr, rfd1, rfd2) <- runRight $ testSend sndr filePath
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
(sfId, sndDescr, rfd1, rfd2) <- runRight $ testSend sndr filePath
|
||||
|
||||
-- receive file
|
||||
rcp1 <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ . void $
|
||||
testReceive rcp1 rfd1 filePath
|
||||
disconnectAgentClient rcp1
|
||||
disconnectAgentClient sndr
|
||||
pure (sfId, sndDescr, rfd2)
|
||||
-- receive file
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp1 ->
|
||||
runRight_ . void $ testReceive rcp1 rfd1 filePath
|
||||
pure (sfId, sndDescr, rfd2)
|
||||
|
||||
-- delete file - should not succeed with server down
|
||||
sndr <- getSMPAgentClient' 3 agentCfg initAgentServers testDB
|
||||
runRight $ do
|
||||
xftpStartWorkers sndr (Just senderFiles)
|
||||
withAgent 3 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
runRight_ $ xftpStartWorkers sndr (Just senderFiles)
|
||||
xftpDeleteSndFileRemote sndr 1 sfId sndDescr
|
||||
liftIO $ timeout 300000 (get sndr) `shouldReturn` Nothing -- wait for worker attempt
|
||||
disconnectAgentClient sndr
|
||||
|
||||
timeout 300000 (get sndr) `shouldReturn` Nothing -- wait for worker attempt
|
||||
threadDelay 300000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
-- delete file - should succeed with server up
|
||||
sndr' <- getSMPAgentClient' 4 agentCfg initAgentServers testDB
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
withAgent 4 agentCfg initAgentServers testDB $ \sndr' -> do
|
||||
runRight_ $ xftpStartWorkers sndr' (Just senderFiles)
|
||||
|
||||
threadDelay 1000000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
threadDelay 1000000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
|
||||
-- receive file - should fail with AUTH error
|
||||
rcp2 <- getSMPAgentClient' 5 agentCfg initAgentServers testDB3
|
||||
runRight $ do
|
||||
xftpStartWorkers rcp2 (Just recipientFiles)
|
||||
rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing
|
||||
("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
-- 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
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
|
||||
testXFTPAgentDeleteOnServer :: HasCallStack => IO ()
|
||||
testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $
|
||||
@@ -536,36 +552,36 @@ testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $
|
||||
filePath1 <- createRandomFile' "testfile1"
|
||||
|
||||
-- send file 1
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
(_, _, rfd1_1, rfd1_2) <- runRight $ testSend sndr filePath1
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
(_, _, rfd1_1, rfd1_2) <- runRight $ testSend sndr filePath1
|
||||
|
||||
-- receive file 1 successfully
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ . void $
|
||||
testReceive rcp rfd1_1 filePath1
|
||||
-- receive file 1 successfully
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
runRight_ . void $ testReceive rcp rfd1_1 filePath1
|
||||
|
||||
serverFiles <- listDirectory xftpServerFiles
|
||||
length serverFiles `shouldBe` 6
|
||||
serverFiles <- listDirectory xftpServerFiles
|
||||
length serverFiles `shouldBe` 6
|
||||
|
||||
-- delete file 1 on server from file system
|
||||
forM_ serverFiles (\file -> removeFile (xftpServerFiles </> file))
|
||||
-- delete file 1 on server from file system
|
||||
forM_ serverFiles (\file -> removeFile (xftpServerFiles </> file))
|
||||
|
||||
threadDelay 1000000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
threadDelay 1000000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
|
||||
-- create and send file 2
|
||||
filePath2 <- createRandomFile' "testfile2"
|
||||
(_, _, rfd2, _) <- runRight $ testSend sndr filePath2
|
||||
-- create and send file 2
|
||||
filePath2 <- createRandomFile' "testfile2"
|
||||
(_, _, rfd2, _) <- runRight $ testSend sndr filePath2
|
||||
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
|
||||
runRight_ . void $ do
|
||||
-- receive file 1 again
|
||||
-- TODO should fail with AUTH error
|
||||
_rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing
|
||||
runRight_ . void $ do
|
||||
-- receive file 1 again
|
||||
rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing
|
||||
("", rfId1', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp
|
||||
liftIO $ rfId1 `shouldBe` rfId1'
|
||||
|
||||
-- receive file 2
|
||||
testReceive' rcp rfd2 filePath2
|
||||
-- receive file 2
|
||||
testReceive' rcp rfd2 filePath2
|
||||
|
||||
testXFTPAgentExpiredOnServer :: HasCallStack => IO ()
|
||||
testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do
|
||||
@@ -574,46 +590,43 @@ testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do
|
||||
filePath1 <- createRandomFile' "testfile1"
|
||||
|
||||
-- send file 1
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
(_, _, rfd1_1, rfd1_2) <- runRight $ testSend sndr filePath1
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \sndr -> do
|
||||
(_, _, rfd1_1, rfd1_2) <- runRight $ testSend sndr filePath1
|
||||
|
||||
-- receive file 1 successfully
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ . void $
|
||||
testReceive rcp rfd1_1 filePath1
|
||||
-- receive file 1 successfully
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do
|
||||
runRight_ . void $ testReceive rcp rfd1_1 filePath1
|
||||
|
||||
serverFiles <- listDirectory xftpServerFiles
|
||||
length serverFiles `shouldBe` 6
|
||||
serverFiles <- listDirectory xftpServerFiles
|
||||
length serverFiles `shouldBe` 6
|
||||
|
||||
-- wait until file 1 expires on server
|
||||
forM_ serverFiles (\file -> removeFile (xftpServerFiles </> file))
|
||||
-- wait until file 1 expires on server
|
||||
forM_ serverFiles (\file -> removeFile (xftpServerFiles </> file))
|
||||
|
||||
threadDelay 3500000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
threadDelay 3500000
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 0
|
||||
|
||||
-- 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
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
-- 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
|
||||
liftIO $ rfId' `shouldBe` rfId
|
||||
|
||||
-- create and send file 2
|
||||
filePath2 <- createRandomFile' "testfile2"
|
||||
(_, _, rfd2, _) <- runRight $ testSend sndr filePath2
|
||||
-- create and send file 2
|
||||
filePath2 <- createRandomFile' "testfile2"
|
||||
(_, _, rfd2, _) <- runRight $ testSend sndr filePath2
|
||||
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
length <$> listDirectory xftpServerFiles `shouldReturn` 6
|
||||
|
||||
-- receive file 2 successfully
|
||||
runRight_ . void $
|
||||
testReceive' rcp rfd2 filePath2
|
||||
-- receive file 2 successfully
|
||||
runRight_ . void $ testReceive' rcp rfd2 filePath2
|
||||
|
||||
testXFTPAgentRequestAdditionalRecipientIDs :: HasCallStack => IO ()
|
||||
testXFTPAgentRequestAdditionalRecipientIDs = withXFTPServer $ do
|
||||
filePath <- createRandomFile
|
||||
|
||||
-- send file
|
||||
sndr <- getSMPAgentClient' 1 agentCfg initAgentServers testDB
|
||||
rfds <- runRight $ do
|
||||
rfds <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do
|
||||
xftpStartWorkers sndr (Just senderFiles)
|
||||
sfId <- xftpSendFile sndr 1 (CF.plain filePath) 500
|
||||
sfProgress sndr $ mb 18
|
||||
@@ -623,10 +636,11 @@ testXFTPAgentRequestAdditionalRecipientIDs = withXFTPServer $ do
|
||||
length rfds `shouldBe` 500
|
||||
pure rfds
|
||||
|
||||
forM_ rfds testNoRedundancy
|
||||
|
||||
-- receive file using different descriptions
|
||||
-- ! revise number of recipients and indexes if xftpMaxRecipientsPerRequest is changed
|
||||
rcp <- getSMPAgentClient' 2 agentCfg initAgentServers testDB2
|
||||
runRight_ $ do
|
||||
withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight_ $ do
|
||||
void $ testReceive rcp (head rfds) filePath
|
||||
void $ testReceive rcp (rfds !! 99) filePath
|
||||
void $ testReceive rcp (rfds !! 299) filePath
|
||||
@@ -634,6 +648,7 @@ testXFTPAgentRequestAdditionalRecipientIDs = withXFTPServer $ do
|
||||
|
||||
testXFTPServerTest :: HasCallStack => Maybe BasicAuth -> XFTPServerWithAuth -> IO (Maybe ProtocolTestFailure)
|
||||
testXFTPServerTest newFileBasicAuth srv =
|
||||
withXFTPServerCfg testXFTPServerConfig {newFileBasicAuth, xftpPort = xftpTestPort2} $ \_ -> do
|
||||
a <- getSMPAgentClient' 1 agentCfg initAgentServers testDB -- initially passed server is not running
|
||||
runRight $ testProtocolServer a 1 srv
|
||||
withXFTPServerCfg testXFTPServerConfig {newFileBasicAuth, xftpPort = xftpTestPort2} $ \_ ->
|
||||
-- initially passed server is not running
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a ->
|
||||
testProtocolServer a 1 srv
|
||||
|
||||
+18
-7
@@ -1,19 +1,21 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module XFTPClient where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Concurrent (ThreadId, threadDelay)
|
||||
import Data.String (fromString)
|
||||
import Network.Socket (ServiceName)
|
||||
import SMPClient (serverBracket)
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Server (runXFTPServerBlocking)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes)
|
||||
import Simplex.Messaging.Protocol (XFTPServer)
|
||||
import Simplex.Messaging.Transport (ALPN)
|
||||
import Simplex.Messaging.Transport.Server
|
||||
import Test.Hspec
|
||||
|
||||
@@ -52,7 +54,7 @@ withXFTPServerCfg :: HasCallStack => XFTPServerConfig -> (HasCallStack => Thread
|
||||
withXFTPServerCfg cfg =
|
||||
serverBracket
|
||||
(`runXFTPServerBlocking` cfg)
|
||||
(pure ())
|
||||
(threadDelay 10000)
|
||||
|
||||
withXFTPServerThreadOn :: HasCallStack => (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withXFTPServerThreadOn = withXFTPServerCfg testXFTPServerConfig
|
||||
@@ -94,7 +96,10 @@ testXFTPStatsBackupFile :: FilePath
|
||||
testXFTPStatsBackupFile = "tests/tmp/xftp-server-stats.log"
|
||||
|
||||
testXFTPServerConfig :: XFTPServerConfig
|
||||
testXFTPServerConfig =
|
||||
testXFTPServerConfig = testXFTPServerConfig_ (Just supportedXFTPhandshakes)
|
||||
|
||||
testXFTPServerConfig_ :: Maybe [ALPN] -> XFTPServerConfig
|
||||
testXFTPServerConfig_ alpn =
|
||||
XFTPServerConfig
|
||||
{ xftpPort = xftpTestPort,
|
||||
controlPort = Nothing,
|
||||
@@ -105,7 +110,10 @@ testXFTPServerConfig =
|
||||
allowedChunkSizes = [kb 64, kb 128, kb 256, mb 1, mb 4],
|
||||
allowNewFiles = True,
|
||||
newFileBasicAuth = Nothing,
|
||||
controlPortAdminAuth = Nothing,
|
||||
controlPortUserAuth = Nothing,
|
||||
fileExpiration = Just defaultFileExpiration,
|
||||
fileTimeout = 10000000,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
@@ -114,14 +122,17 @@ testXFTPServerConfig =
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
transportConfig = defaultTransportServerConfig
|
||||
transportConfig = defaultTransportServerConfig {alpn}
|
||||
}
|
||||
|
||||
testXFTPClientConfig :: XFTPClientConfig
|
||||
testXFTPClientConfig = defaultXFTPClientConfig
|
||||
|
||||
testXFTPClient :: HasCallStack => (HasCallStack => XFTPClient -> IO a) -> IO a
|
||||
testXFTPClient client =
|
||||
getXFTPClient (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> pure ()) >>= \case
|
||||
testXFTPClient = testXFTPClientWith testXFTPClientConfig
|
||||
|
||||
testXFTPClientWith :: HasCallStack => XFTPClientConfig -> (HasCallStack => XFTPClient -> IO a) -> IO a
|
||||
testXFTPClientWith cfg client =
|
||||
getXFTPClient (1, testXFTPServer, Nothing) cfg (\_ -> pure ()) >>= \case
|
||||
Right c -> client c
|
||||
Left e -> error $ show e
|
||||
|
||||
@@ -22,13 +22,12 @@ import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Description (kb)
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..), XFTPErrorType (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPRcvChunkSpec (..))
|
||||
import Simplex.Messaging.Client (ProtocolClientError (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Protocol (BasicAuth, SenderId)
|
||||
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
|
||||
import Simplex.Messaging.Util (liftIOEither)
|
||||
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive, removeFile)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
@@ -60,6 +59,7 @@ xftpServerTests =
|
||||
it "prohibited when FNEW disabled" $ testFileBasicAuth False (Just "pwd") (Just "pwd") False
|
||||
it "allowed with correct basic auth" $ testFileBasicAuth True (Just "pwd") (Just "pwd") True
|
||||
it "allowed with auth on server without auth" $ testFileBasicAuth True Nothing (Just "any") True
|
||||
it "should not change content for uploaded and committed files" testFileSkipCommitted
|
||||
|
||||
chSize :: Integral a => a
|
||||
chSize = kb 128
|
||||
@@ -219,7 +219,7 @@ testFileChunkExpiration = withXFTPServerCfg testXFTPServerConfig {fileExpiration
|
||||
testInactiveClientExpiration :: Expectation
|
||||
testInactiveClientExpiration = withXFTPServerCfg testXFTPServerConfig {inactiveClientExpiration} $ \_ -> runRight_ $ do
|
||||
disconnected <- newEmptyTMVarIO
|
||||
c <- liftIOEither $ getXFTPClient (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> atomically $ putTMVar disconnected ())
|
||||
c <- ExceptT $ getXFTPClient (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> atomically $ putTMVar disconnected ())
|
||||
pingXFTP c
|
||||
liftIO $ do
|
||||
threadDelay 100000
|
||||
@@ -372,3 +372,22 @@ testFileBasicAuth allowNewFiles newFileBasicAuth clntAuth success =
|
||||
else do
|
||||
void (createXFTPChunk c spKey file [rcvKey] clntAuth)
|
||||
`catchError` (liftIO . (`shouldBe` PCEProtocolError AUTH))
|
||||
|
||||
testFileSkipCommitted :: IO ()
|
||||
testFileSkipCommitted =
|
||||
withXFTPServerCfg testXFTPServerConfig $
|
||||
\_ -> testXFTPClient $ \c -> do
|
||||
g <- C.newRandom
|
||||
(sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(rcvKey, rpKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
bytes <- createTestChunk testChunkPath
|
||||
digest <- LC.sha256Hash <$> LB.readFile testChunkPath
|
||||
let file = FileInfo {sndKey, size = chSize, digest}
|
||||
chunkSpec = XFTPChunkSpec {filePath = testChunkPath, chunkOffset = 0, chunkSize = chSize}
|
||||
runRight_ $ do
|
||||
(sId, [rId]) <- createXFTPChunk c spKey file [rcvKey] Nothing
|
||||
uploadXFTPChunk c spKey sId chunkSpec
|
||||
void . liftIO $ createTestChunk testChunkPath -- trash chunk contents
|
||||
uploadXFTPChunk c spKey sId chunkSpec -- upload again to get FROk without getting stuck
|
||||
downloadXFTPChunk g c rpKey rId $ XFTPRcvChunkSpec "tests/tmp/received_chunk" chSize digest
|
||||
liftIO $ B.readFile "tests/tmp/received_chunk" `shouldReturn` bytes -- new chunk content got ignored
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBtjCCATagAwIBAgIUDJc0ixVBYPdcL5W7zE8dhm0UMpswBQYDK2VxMCoxFjAU
|
||||
BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjQwNDA4
|
||||
MTg0ODIwWhgPNDc2MjAzMDUxODQ4MjBaMCoxFjAUBgNVBAMMDVNNUCBzZXJ2ZXIg
|
||||
Q0ExEDAOBgNVBAoMB1NpbXBsZVgwQzAFBgMrZXEDOgAST4assVdIwL/kbtWmbyJm
|
||||
X/CNGUQFkArvgvcRTZOwJPu9ypmv0mSz2I6acsw6gr8LHq8mlv7iPICjUzBRMB0G
|
||||
A1UdDgQWBBTBsA6VVhkO61ixwlel+g7D08shnjAfBgNVHSMEGDAWgBTBsA6VVhkO
|
||||
61ixwlel+g7D08shnjAPBgNVHRMBAf8EBTADAQH/MAUGAytlcQNzAKAfQ0EEQtnR
|
||||
HvNiKBajo77prZX680apmxBxSZuLNORQMvKBLDm2qaGv5S/c9gmvLjLz2Avrspow
|
||||
ANBF71DKcvgb25D2LLDp0CQOBt/dP41Cgd/ZigyHyOq2/Oj15Skbu0TdXYuIxf/k
|
||||
MZ0XUvYwG6IKAA==
|
||||
-----END CERTIFICATE-----
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MEcCAQAwBQYDK2VxBDsEOcBpozc2TnAf6lQaxN5bA6JdbKWuxUecsW9P2dzncCnB
|
||||
/alBtYXqW6SprBj1DqzeZyU4rQ7OqFrgBw==
|
||||
-----END PRIVATE KEY-----
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBvDCCATygAwIBAgIUbx6kKw7PGGxhTPutroJFZbOcVk0wBQYDK2VxMCoxFjAU
|
||||
BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjQwNDA4
|
||||
MTg0ODI2WhgPNDc2MjAzMDUxODQ4MjZaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDBD
|
||||
MAUGAytlcQM6ACKox7DzkUjK6ZN0pCzABv5vcqk5Tu+zaLWEWlHFnpIN/f/AcBI1
|
||||
GbZCmD/zb6OG49vsAKPnMAyIgKNvMG0wCQYDVR0TBAIwADALBgNVHQ8EBAMCA8gw
|
||||
EwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFGFx/ISB2xEW2tGhVYVncWTd
|
||||
lwkrMB8GA1UdIwQYMBaAFMGwDpVWGQ7rWLHCV6X6DsPTyyGeMAUGAytlcQNzAMp9
|
||||
EL+22OkeGG6s7LxpXJgVG6dxbcNn6aTgTX2pDYt8n+cRQTeTZ1MLDYVIe289pIQK
|
||||
tbKmI+HIgHExuNurJw6f6FknVmEeJpOXLV5lybL4f/fZGKrAE5rbhtNnQAp1mw0c
|
||||
ngt8dhyISxv/zoQLSkIcAA==
|
||||
-----END CERTIFICATE-----
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MEcCAQAwBQYDK2VxBDsEObrN+1gIRcwmahmitb6ltVoZjjnVoHj0/1waYkjmMtQl
|
||||
PiGhWP5/B6Y1fLH/YiO/tfX2YPGCOJJSJQ==
|
||||
-----END PRIVATE KEY-----
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB6zCCAXKgAwIBAgIUbLI6PjnyP24ukjmIE3LsjFqn/LYwCgYIKoZIzj0EAwIw
|
||||
FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjQwNDA4MTg1OTA2WhcNMzQwNDA2
|
||||
MTg1OTA2WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTB2MBAGByqGSM49AgEGBSuB
|
||||
BAAiA2IABMIrmyP4FDY+P8Tulv8Bcp5U7QlHigoOW6JPRPTETTFBl2e7t9UApa/E
|
||||
AYl805mkaIdrDJzdtAqkttHmPm4vXdCCualxVRZ/thtpvdNocxyJOD9BVv3QKqiu
|
||||
SGuCGHp+m6OBgDB+MB0GA1UdDgQWBBR5D79SM77XfY/bii0NJ1OllAWw1TAfBgNV
|
||||
HSMEGDAWgBR5D79SM77XfY/bii0NJ1OllAWw1TAPBgNVHRMBAf8EBTADAQH/MCsG
|
||||
A1UdEQQkMCKCC2V4YW1wbGUuY29tgg0qLmV4YW1wbGUuY29thwQKAAABMAoGCCqG
|
||||
SM49BAMCA2cAMGQCMDuIOZBcKI/OXOWx75o5xgwIDio4P7zK9kJt7D4YJMxPvTV6
|
||||
vVajYSuJwiIF3/GwoQIwPUbNndNNnf1tYJdPhEJ3e8bA2a3bDbb2dgfiUfj6amaS
|
||||
RcSkYms1WDLMFP0LHo/Z
|
||||
-----END CERTIFICATE-----
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBH/6IpMqcKFrXNU8Nb
|
||||
QFvdzQOJtfoAEEDRBmMqbihPrCgbtCJ3FIVnxqGlFIXADaqhZANiAATCK5sj+BQ2
|
||||
Pj/E7pb/AXKeVO0JR4oKDluiT0T0xE0xQZdnu7fVAKWvxAGJfNOZpGiHawyc3bQK
|
||||
pLbR5j5uL13QgrmpcVUWf7Ybab3TaHMciTg/QVb90Cqorkhrghh6fps=
|
||||
-----END PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user