mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-01 07:18:23 +00:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04e4a37d85 | ||
|
|
0ba3e69872 | ||
|
|
2ab0c2a7c6 | ||
|
|
243548631a | ||
|
|
da79d544cf | ||
|
|
e808825c95 | ||
|
|
a9576935cf | ||
|
|
9cf6c97137 | ||
|
|
0739f7b702 | ||
|
|
e12710fa55 | ||
|
|
2a120dfe57 | ||
|
|
3c18c4b66a | ||
|
|
21eee2b548 | ||
|
|
03168b9fbf | ||
|
|
41e1b2bd39 | ||
|
|
1afa0206a5 | ||
|
|
81fcdf8ac9 | ||
|
|
1f67b403de | ||
|
|
8e7f3f7b27 | ||
|
|
e6e9f1d5a5 | ||
|
|
4268b90763 | ||
|
|
856137b763 | ||
|
|
d0ee505c3a | ||
|
|
7dcac19a67 | ||
|
|
22260cd719 | ||
|
|
bef11e4cbe | ||
|
|
d5a65656f5 | ||
|
|
309ef3766c | ||
|
|
bde7b97f6a | ||
|
|
398dbeedea | ||
|
|
fa772af6c6 | ||
|
|
9ebdd2b0ea | ||
|
|
bec4e5e038 | ||
|
|
ea67b34ba7 | ||
|
|
e247f691cb | ||
|
|
896b2425a4 | ||
|
|
30e15b881f | ||
|
|
24ded9e5a0 | ||
|
|
f5e666ae4f | ||
|
|
62133ceb24 | ||
|
|
3b50e1fb7d | ||
|
|
7c25b3b1e0 | ||
|
|
a70bd02c67 | ||
|
|
7000431249 |
@@ -1,3 +1,31 @@
|
||||
# 6.0.5
|
||||
|
||||
SMP agent:
|
||||
- support generic SOCKS proxy (without isolate-by-auth).
|
||||
- reduce max message sizes
|
||||
|
||||
# 6.0.4
|
||||
|
||||
SMP server:
|
||||
- better performance/memory: fewer map updates on re-subscriptions (#1297), split and reduce STM transactions (#1294)
|
||||
- send DELD when subscribed queue is deleted (#1312)
|
||||
- add created/updated/used date to queues to manage expiration (#1306)
|
||||
|
||||
XFTP server: truncate file creation time to 1 hour (#1310)
|
||||
|
||||
Servers:
|
||||
- bind control port only to 127.0.0.1 for better security in case of firewall misconfiguration (#1280)
|
||||
- reduce memory used for period stats (#1298)
|
||||
|
||||
Agent: process last notification from list (#1307)
|
||||
- report receive file error with redirected file ID, when redirect is present (#1304)
|
||||
- special error when deleted user record is not in database (#1303)
|
||||
- fix race when sending a message to the deleted connection (#1296)
|
||||
- support for multiple messages in a single notification
|
||||
|
||||
Ntf server:
|
||||
- only use SOCKS proxy for servers without public address (#1314)
|
||||
|
||||
# 6.0.3
|
||||
|
||||
Agent:
|
||||
|
||||
@@ -19,4 +19,4 @@ main = do
|
||||
setLogLevel LogDebug
|
||||
cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath
|
||||
logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath
|
||||
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath
|
||||
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles Static.attachStaticFiles cfgPath logPath
|
||||
|
||||
@@ -221,6 +221,10 @@
|
||||
Public information
|
||||
</h2>
|
||||
<table id="public-info">
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Server version:</td>
|
||||
<td>${version}</td>
|
||||
</tr>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Source code:</td>
|
||||
<td><a href="${sourceCode}" target="_blank">${sourceCode}</a></td>
|
||||
|
||||
@@ -7,22 +7,29 @@ module Static where
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.IORef (readIORef)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.String (fromString)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Wai.Application.Static as S
|
||||
import Network.Wai.Handler.Warp as W
|
||||
import qualified Network.Wai.Handler.WarpTLS as W
|
||||
import Network.Socket (getPeerName)
|
||||
import Network.Wai (Application)
|
||||
import qualified Network.Wai.Application.Static as S
|
||||
import qualified Network.Wai.Handler.Warp as W
|
||||
import qualified Network.Wai.Handler.Warp.Internal as WI
|
||||
import qualified Network.Wai.Handler.WarpTLS as WT
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Server (AttachHTTP)
|
||||
import Simplex.Messaging.Server.Information
|
||||
import Simplex.Messaging.Server.Main (EmbeddedWebParams (..), WebHttpsParams (..))
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import Static.Embedded as E
|
||||
import System.Directory (createDirectoryIfMissing)
|
||||
import System.FilePath
|
||||
import UnliftIO.Concurrent (forkFinally)
|
||||
import UnliftIO.Exception (bracket, finally)
|
||||
|
||||
serveStaticFiles :: EmbeddedWebParams -> IO ()
|
||||
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do
|
||||
@@ -31,9 +38,42 @@ serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams}
|
||||
W.runSettings (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
|
||||
forM_ webHttpsParams $ \WebHttpsParams {port, cert, key} -> flip forkFinally (\e -> logError $ "HTTPS server crashed: " <> tshow e) $ do
|
||||
logInfo $ "Serving static site on port " <> tshow port <> " (TLS)"
|
||||
W.runTLS (W.tlsSettings cert key) (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
|
||||
WT.runTLS (WT.tlsSettings cert key) (mkSettings port) app
|
||||
where
|
||||
mkSettings port = setPort port defaultSettings
|
||||
app = staticFiles webStaticPath
|
||||
mkSettings port = W.setPort port W.defaultSettings
|
||||
|
||||
-- | Prepare context and prepare HTTP handler for TLS connections that already passed TLS.handshake and ALPN check.
|
||||
attachStaticFiles :: FilePath -> (AttachHTTP -> IO ()) -> IO ()
|
||||
attachStaticFiles path action =
|
||||
-- Initialize global internal state for http server.
|
||||
WI.withII settings $ \ii -> do
|
||||
action $ \socket cxt -> do
|
||||
-- Initialize internal per-connection resources.
|
||||
addr <- getPeerName socket
|
||||
withConnection addr cxt $ \(conn, transport) ->
|
||||
withTimeout ii conn $ \th ->
|
||||
-- Run Warp connection handler to process HTTP requests for static files.
|
||||
WI.serveConnection conn ii th addr transport settings app
|
||||
where
|
||||
app = staticFiles path
|
||||
settings = W.defaultSettings
|
||||
-- from warp-tls
|
||||
withConnection socket cxt = bracket (WT.attachConn socket cxt) (terminate . fst)
|
||||
-- from warp
|
||||
withTimeout ii conn =
|
||||
bracket
|
||||
(WI.registerKillThread (WI.timeoutManager ii) (WI.connClose conn))
|
||||
WI.cancel
|
||||
-- shared clean up
|
||||
terminate conn = WI.connClose conn `finally` (readIORef (WI.connWriteBuffer conn) >>= WI.bufFree)
|
||||
|
||||
staticFiles :: FilePath -> Application
|
||||
staticFiles root = S.staticApp settings
|
||||
where
|
||||
settings = (S.defaultFileServerSettings root)
|
||||
{ S.ssListing = Nothing
|
||||
}
|
||||
|
||||
generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO ()
|
||||
generateSite si onionHost sitePath = do
|
||||
@@ -78,6 +118,7 @@ serverInformation ServerInformation {config, information} onionHost = render E.i
|
||||
where
|
||||
basic =
|
||||
[ ("sourceCode", Just . encodeUtf8 $ sourceCode spi),
|
||||
("version", Just $ B.pack simplexMQVersion),
|
||||
("website", encodeUtf8 <$> website spi)
|
||||
]
|
||||
conds ServerConditions {conditions, amendments} =
|
||||
|
||||
@@ -28,3 +28,17 @@ source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/sqlcipher-simple.git
|
||||
tag: a46bd361a19376c5211f1058908fc0ae6bf42446
|
||||
|
||||
-- waiting for published warp-tls-3.4.7
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/yesodweb/wai.git
|
||||
tag: ec5e017d896a78e787a5acea62b37a4e677dec2e
|
||||
subdir: warp-tls
|
||||
|
||||
-- backported fork due http-5.0
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/wai.git
|
||||
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
|
||||
subdir: warp
|
||||
|
||||
+17
-6
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 6.0.3.0
|
||||
version: 6.1.0.4
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
@@ -123,9 +123,10 @@ executables:
|
||||
dependencies:
|
||||
- file-embed
|
||||
- simplexmq
|
||||
- wai
|
||||
- wai-app-static
|
||||
- warp
|
||||
- warp-tls
|
||||
- warp ==3.3.30 # the last one before http2-5.0
|
||||
- warp-tls ==3.4.7 # extra internals exposed
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
@@ -159,19 +160,29 @@ executables:
|
||||
|
||||
tests:
|
||||
simplexmq-test:
|
||||
source-dirs: tests
|
||||
source-dirs:
|
||||
- tests
|
||||
- apps/smp-server/web
|
||||
main: Test.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
- deepseq == 1.4.*
|
||||
- file-embed
|
||||
- generic-random == 1.5.*
|
||||
- hspec == 2.11.*
|
||||
- hspec-core == 2.11.*
|
||||
- http-client
|
||||
- http-client-tls
|
||||
- HUnit == 1.6.*
|
||||
- main-tester == 0.2.*
|
||||
- QuickCheck == 2.14.*
|
||||
- silently == 1.2.*
|
||||
- main-tester == 0.2.*
|
||||
- simplexmq
|
||||
- timeit == 2.0.*
|
||||
- unordered-containers
|
||||
- wai
|
||||
- wai-app-static
|
||||
- warp
|
||||
- warp-tls
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
|
||||
+27
-7
@@ -67,7 +67,7 @@ The session invitation contains this data:
|
||||
- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
|
||||
- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
|
||||
- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity.
|
||||
- Session X25519 DH key and SNTRUP761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption.
|
||||
- Session X25519 DH key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption.
|
||||
|
||||
Host application decrypts (except the first session) and validates the invitation:
|
||||
- Session signature is valid.
|
||||
@@ -184,7 +184,7 @@ The controller decrypts (including the first session) and validates the received
|
||||
The controller should reply with with `ctrlHello` or `ctrlError` response:
|
||||
|
||||
```abnf
|
||||
ctrlHello = %s"HELLO " kemCiphertext nonce encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
|
||||
ctrlHello = %s"HELLO " kemCiphertext encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
|
||||
; ctrlHelloJSON is encrypted with the hybrid secret,
|
||||
; including both previously agreed DH secret and KEM secret from kemCiphertext
|
||||
unpaddedSize = largeLength
|
||||
@@ -206,6 +206,8 @@ JTD schema for the encrypted part of controller HELLO block `ctrlHelloJSON`:
|
||||
}
|
||||
```
|
||||
|
||||
Controller `hello` block and all subsequent protocol messages are encrypted with the chain keys derived from the hybrid key (see key exchange below) - that is why conntroller hello block does not include nonce. That provides forward secrecy within the XRCP session. Receiving this `hello` block allows host to compute the same hybrid keys and to derive the same chain keys.
|
||||
|
||||
Once the controller replies HELLO to the valid host HELLO block, it should stop accepting new TCP connections.
|
||||
|
||||
### Controller/host session operation
|
||||
@@ -223,10 +225,12 @@ tlsunique channel binding from TLS session MUST be included in commands (include
|
||||
The syntax for encrypted command and response body encoding:
|
||||
|
||||
```abnf
|
||||
commandBody = encBody sessSignature idSignature [attachment]
|
||||
responseBody = encBody [attachment] ; counter must match command
|
||||
encBody = nonce encLength32 encrypted(tlsunique counter body)
|
||||
attachment = %x01 nonce encLength32 encrypted(attachment)
|
||||
commandBody = counter encBody sessSignature idSignature [attachment]
|
||||
responseBody = counter encBody [attachment] ; counter must match command
|
||||
; counter is placed outside of encrypted body to allow correlating encryption keys
|
||||
; with the chain keys (each command and response are encrypted by different keys)
|
||||
encBody = encLength32 encrypted(tlsunique body)
|
||||
attachment = %x01 encLength32 encrypted(attachment)
|
||||
noAttachment = %x00
|
||||
tlsunique = length 1*OCTET
|
||||
counter = 8*8 OCTET ; int64
|
||||
@@ -239,7 +243,7 @@ If the command or response includes attachment, its hash must be included in com
|
||||
|
||||
Initial announcement is shared out-of-band (URI with xrcp scheme), and it is not encrypted.
|
||||
|
||||
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA256 over concatenated DH shared secret and KEM encapsulated secret) both for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses.
|
||||
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA3-256 over concatenated DH shared secret and KEM encapsulated secret) to derive keys for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses.
|
||||
|
||||
During the next session the announcement is sent via encrypted multicast block. The shared key for this announcement and for host HELLO block is determined using the KEM shared secret from the previous session and DH shared secret computed using the host DH key from the previous session and the new controller DH key from the announcement.
|
||||
|
||||
@@ -273,6 +277,22 @@ If controller fails to store the new host DH key after receiving HELLO block, th
|
||||
|
||||
To decrypt a multicast announcement, the host should try to decrypt it using the keys of all known (paired) remote controllers.
|
||||
|
||||
Once kemSecret is agreed for the session, it is used to derive two chain keys, to receive and to send messages:
|
||||
|
||||
```
|
||||
host: sndKey, rcvKey = HKDF(kemSecret, "SimpleXSbChainInit", 64)
|
||||
controller: rcvKey, sndKey = HKDF(kemSecret, "SimpleXSbChainInit", 64)
|
||||
```
|
||||
|
||||
where HKDF is based on SHA512, with empty salt.
|
||||
|
||||
Actual keys and nonces to encrypt and decrypt messages are derived from these chain keys:
|
||||
|
||||
```
|
||||
to send: (sndKey', sk, nonce) = HKDF(sndKey, "SimpleXSbChain", 88)
|
||||
to receive: (rcvKey', sk, nonce) = HKDF(rcvKey, "SimpleXSbChain", 88)
|
||||
```
|
||||
|
||||
## Threat model
|
||||
|
||||
#### A passive network adversary able to monitor the site-local traffic:
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# SMP server message storage
|
||||
|
||||
## Problem
|
||||
|
||||
Currently SMP servers store all queues in server memory. As the traffic grows, so does the number of undelivered messages. What is worse, Haskell is not avoiding heap fragmentation when messages are allocated and then de-allocated - undelivered messages use ByteString and GC cannot move them around, as they use pinned memory.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
### Solution 1: solve only GC fragmentation problem
|
||||
|
||||
Move from ByteString to some other primitive to store messages in memory long term, e.g. ShortByteString, or manage allocation/de-allocation of stored messages manually in some other way.
|
||||
|
||||
Pros: the simplest solution that avoids substantial re-engineering of the server.
|
||||
|
||||
Cons:
|
||||
- not a long term solution, as memory growth still has limits.
|
||||
- may be ineffective, as it introduces additional copying of bytes.
|
||||
|
||||
### Solution 2: move message storage to hard drive
|
||||
|
||||
Use files or RocksDB to store messages.
|
||||
|
||||
Pros:
|
||||
- much lower memory usage.
|
||||
- no message loss in case of abnormal server termination (important until clients have delivery redundancy).
|
||||
- this is a long term solution, and at some point it might need to be done anyway.
|
||||
|
||||
Cons:
|
||||
- substantial re-engineering costs and risks.
|
||||
- metadata privacy. Currently we only save undelivered messages when server is restarted, with this approach all messages will be stored for some time. this argument is limited, as hosting providers of VMs can make memory snapshots too, on the other hand they are harder to analyze than files. On another hand, with this approach messages will be stored for a shorter time.
|
||||
|
||||
#### RocksDB and other key-value stores
|
||||
|
||||
The downside of any key-value stores is that they don't seem to have efficient primitives for sequential delivery. While sequential delivery can be modelled with linked lists, they would require 1 key insert (on send), 3 key updates (1 update to update queue data on send, 1 update of the last message to point to the next, 1 update on delivery or message expiration) and 1 key deletion (on delivery or message expiration) for each delivered message.
|
||||
|
||||
This might result in substantial write amplification and compacting costs.
|
||||
|
||||
In general, tree structures that are efficient for quick lookups and updates, given approximately fixed value size, are inefficient for modelling queues.
|
||||
|
||||
#### Files
|
||||
|
||||
The upside of files is that they are well suited for sequential delivery and don't result in the same churn, with careful design, as trees do.
|
||||
|
||||
The downside of filesystem is that it does not scale well with the large number of files in a folder, so queues will have to be spread across multiple folders, following tree-like structure.
|
||||
|
||||
I could not find an available library that efficiently models sequential delivery in highly concurrent environment.
|
||||
|
||||
A possible design could be the following.
|
||||
|
||||
##### Queue folder and files
|
||||
|
||||
Each message queue is stored in its own folder (see below on folder locations). Folder would contain these files:
|
||||
|
||||
- `messages.abcd.log` - the file that is used to read messages from, sequentially
|
||||
- `messages.efgh.log` - the optional file that is used to write messages, in case it is different from read file.
|
||||
- `queue.log` - append-only file where the last line represents the current queue state
|
||||
- `queue.timestamp.log` - previous states of queue.log file
|
||||
|
||||
Each line in "queue.log" file has this syntax
|
||||
|
||||
```abnf
|
||||
queueLogLine =
|
||||
%s"read_file=" base64
|
||||
%s"read_msg=" digits
|
||||
%s"read_byte=" digits
|
||||
%s"write_file=" base64
|
||||
%s"write_msg=" digits
|
||||
```
|
||||
|
||||
When queue is first requested by the server:
|
||||
|
||||
```c
|
||||
if queue folder exists:
|
||||
read queue state from last line of queue.log
|
||||
if queue.log contained more than one line: // compaction
|
||||
copy queue.log to queue.timestamp.log
|
||||
write one line queue state to queue.log
|
||||
else:
|
||||
create queue folder
|
||||
create messages.abcd.log (abcd is some random string)
|
||||
read_msg = 0
|
||||
read_byte = 0
|
||||
create queue.log with one line: "read_file=abcd read_msg=0 read_byte=0 write_files=abcd write_msg=0"
|
||||
open read_file in ReadMode and seek to read_byte position
|
||||
nextReadByte = read_byte
|
||||
nextReadMsg = read_msg
|
||||
open write_file in AppendMode
|
||||
```
|
||||
|
||||
When message is added to the queue (assumes that queue state is loaded to server memory, if not the previous section will be done first):
|
||||
|
||||
```c
|
||||
if write_msg > max_queue_messages:
|
||||
return quota error
|
||||
else if write_msg = max_queue_messages:
|
||||
add quota_exceeded message to write_file
|
||||
update queue state: write_msg += 1
|
||||
append updated queue state to queue.log
|
||||
else ]
|
||||
// It is required that `max_queue_messages < max_file_messages`,
|
||||
// so that we never need more than one additional write file.
|
||||
if write_msg >= max_file_messages: // queue file rotation
|
||||
create messages.efgh.log // efgh is some random string
|
||||
update queue state: write_file=efgh write_msg=0 // read file remains the same as it was
|
||||
append updated queue state to queue.log
|
||||
copy queue.log to queue.timestamp.log
|
||||
// `old` needs to be defined to limit the number and storage duration,
|
||||
// preserving not more than N files, and not more than M days files, "and then some"
|
||||
// (that is if the queue has high churn, we have file from M days before in any case, for any debugging).
|
||||
delete `old` `queue.timestamp.log` files
|
||||
write one line queue state to queue.log // compaction
|
||||
|
||||
add message to write_file
|
||||
update queue state: write_msg += 1
|
||||
append updated queue state to queue.log
|
||||
```
|
||||
|
||||
The above algorithm assumes `max_queue_messages < than max_file_messages`, so that we never need more than one write file.
|
||||
|
||||
When message is delivered, it is simply read from the read queue, queue state does not change yet:
|
||||
|
||||
```c
|
||||
if nextReadMsg > read_msg:
|
||||
deliver cached message, no need to read it again
|
||||
else
|
||||
read message from read_file handle
|
||||
nextReadMsg = read_msg + 1
|
||||
nextReadByte = current position in file
|
||||
```
|
||||
|
||||
When message delivery is acknowledged, the read queue needs to be advanced, and possibly switched to read from the current write_queue:
|
||||
|
||||
```c
|
||||
if nextReadByte == read_byte:
|
||||
return error // nothing was delivered
|
||||
else if nextReadByte = EOF:
|
||||
// end of file is reached, possibly some other condition,
|
||||
// but it should allow changing max_file_messages on server restart
|
||||
currReadFile = read_file
|
||||
read_file = write_file
|
||||
read_msg = 0
|
||||
read_byte = 0
|
||||
append updated queue state to queue.log
|
||||
delete currReadFile
|
||||
else
|
||||
read_msg += 1
|
||||
read_byte = nextReadByte
|
||||
// `seek` should not be necessary, as the handle is already at nextReadByte position here
|
||||
// seek to read_byte
|
||||
append updated queue state to queue.log
|
||||
```
|
||||
|
||||
The above algorithm delegates the problem of compaction and fragmentation management to file system, that is very optimized for such scenarios.
|
||||
|
||||
Also, read and write files will grow to almost a constant size, so the space they used may be re-used.
|
||||
|
||||
An important consideration is that writes to queue.log and message.log files and queue state modifications have to be sequential, without concurrency - it can be managed with the usual locks.
|
||||
|
||||
##### Queue folders structure
|
||||
|
||||
Most Linux systems use EXT4 filesystem where the file lookup time scales linearly to the number of files. While alternatives with logarithmic lookup time exist (XFS), they may be very complex to configure on the existing systems.
|
||||
|
||||
So storing all queue folders in one folder won't scale.
|
||||
|
||||
To solve this problem we could use recipient queue ID in base64url format not as a folder name, but as a folder path, splitting it to path fragments of some length. The number of fragments can be configurable and migration to a different fragment size can be supported as the number of queues on a given server grows.
|
||||
|
||||
Currently, queue ID is 24 bytes random number, thus allowing 2^192 possible queue IDs. If we assume that a server must hold 1b queues, it means that we have ~2^162 possible addresses for each existing queue. 24 bytes in base64 is 32 characters that can be split into say 8 fragments with 4 characters each, so that queue folder path for queue with ID `abcdefghijklmnopqrstuvwxyz012345` would be:
|
||||
|
||||
`/var/opt/simplex/messages/abcd/efgh/ijkl/mnop/qrst/uvwx/yz01/2345`
|
||||
|
||||
The maximum theoretic number of the folders on the 1st level is 64^4, or 2^24 ~ 16m - this is probably still a large number of subfolders for EXT4. Given that addresses are random, all the possible combinations in the first folder can be used with a large number of queues.
|
||||
|
||||
So we could use an unequal split of path, two letters each and the last being long:
|
||||
|
||||
`/var/opt/simplex/messages/ab/cd/ef/ghijklmnopqrstuvwxyz012345`
|
||||
|
||||
The first three levels in this case can have 4096 subfolders each, and it gives 68b possible subfolders (64^2^3), so the last level will be sparse in case of 1b queues on the server. So we could make it 4 levels with 2 letters to never think about it, accounting for a large variance of the random numbers distribution:
|
||||
|
||||
`/var/opt/simplex/messages/ab/cd/ef/gh/ijklmnopqrstuvwxyz012345`
|
||||
@@ -0,0 +1,124 @@
|
||||
# Short invitation links
|
||||
|
||||
## Problem
|
||||
|
||||
Long links look scary and unsafe for many users. While this is a perceived problem, rather than a real one, it hurts adoption.
|
||||
|
||||
What is worse, long links do not fit in profile descriptions of other social networks where people might want to advertize their contact addresses.
|
||||
|
||||
The current link size limitation is also the reason for not including PQ KEM keys into invitation links and addresses, postponing the moment when PQ-resistant encryption kicks in - if we include PQ KEM key into the link, the QR code will not be scannable.
|
||||
|
||||
Additionally, if we store short links, they can also include chat preferences and public profile data.
|
||||
|
||||
## Solution
|
||||
|
||||
MITM-resistant link shortening.
|
||||
|
||||
Instead of generating the random address that would resolve into the link - doing so would create the possibility of MITM by the server hosting this link - we can use private key as the link ID that will be passed to the accepting party, and the hash of the public key as ID for the server - the accepting party would present this key itself as ID and it will also be used for server to client encryption (see Protocol below). HKDF will be used to derive symmetric key from private key and used in secret_box together with random nonce (to allow replacing data with the same key but with a different nonce - nonce will be sent to the server too). secret_box construction is authenticated encryption, so it would protect from MITM.
|
||||
|
||||
The proposed syntax:
|
||||
|
||||
```abnf
|
||||
shortConnectionRequest = connectionScheme "/" connReqType "#/" smpServer "/" linkHash
|
||||
connReqType = %s"invitation" / %s"contact"
|
||||
connectionScheme = (%s"https://" clientAppServer) / %s"simplex:"
|
||||
clientAppServer = hostname [ ":" port ]
|
||||
; client app server, e.g. simplex.chat
|
||||
smpServer = serverIdentity "@" srvHosts [":" port] ; no smp:// prefix, no escaping
|
||||
srvHosts = <hostname> ["," srvHosts] ; RFC1123, RFC5891
|
||||
linkHash = <base64url encoded SHA256 or SHA512 hash of the original link>
|
||||
```
|
||||
|
||||
If SMP server supports pages, its name can be used as clientAppServer, without repeating it after #, for a shorter link.
|
||||
|
||||
Example link:
|
||||
|
||||
```
|
||||
https://simplex.chat/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im/abcdefghij0123456789abcdefghij0123456789abc=
|
||||
```
|
||||
|
||||
This link has the length of ~136 characters (256 bits), which is shorter than the full contact address (~310 characters) and much shorter than invitation links (~528 characters) even without post-quantum keys added to them.
|
||||
|
||||
This size can be further reduced by
|
||||
- use server domain in the link.
|
||||
- do not include onion address, as the connection happens via proxy anyway, if it's untrusted server.
|
||||
- not pinning server TLS certificate - the downside here is that while the attack that compromises TLS will not be able to substitute the link (because it's hash will not match), it will be able to intercept and to block it.
|
||||
- using shorter hash, e.g. SHA128 - reducing the collision resistance.
|
||||
|
||||
If the server is known, the client could use it's hash and onion address, otherwise it could trust the proxy to use any existing session with the same hostname or to accept the risk of interception - given that there is no risk of substitution.
|
||||
|
||||
With the first two of these "improvements" the link could be ~122 characters:
|
||||
|
||||
```
|
||||
https://smp8.simplex.im/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@/abcdefghij0123456789abcdefghij0123456789abc
|
||||
```
|
||||
|
||||
If onion address is preserved the link will be ~184 characters (won't fit in Twitter 160 characters bio):
|
||||
|
||||
```
|
||||
https://smp8.simplex.im/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion/abcdefghij0123456789abcdefghij0123456789abc
|
||||
```
|
||||
|
||||
If we implement it, the request to resolve the link would be made via proxied SMP command (to avoid the direct connection between the client and the recipient's server).
|
||||
|
||||
Pros:
|
||||
- a bit shorter link.
|
||||
- possibility to include post-quantum keys into the full link keeping the same shortened link size.
|
||||
- possibility to include chat profile of contact or group, and preferences, for a much better connection experience, and to show this information when the link sent in the conversation (clients can resolve them automatically, without connecting - it can be resolved by the sending clients).
|
||||
- server will not have access to the link.
|
||||
|
||||
Cons:
|
||||
- protocol complexity.
|
||||
- observers can access the link content, so for 1-time invitation we should only include permissions and not profile.
|
||||
|
||||
Pros are a huge improvement of UX of connecting both within and from outside of the app (e.g., link can be resolved even before creating chat profile, as part of the onboarding).
|
||||
|
||||
## Protocol
|
||||
|
||||
To support short links, the SMP servers would provide a simple key-value store enabled by three additional commands: `WRT`, `CLR` and `READ`
|
||||
|
||||
`WRT` command is used to store and to update values in the store. The size of the value is limited by the same size as sent messages (or, possibly, smaller - as connection information size used in confirmation messages) - the clients would use this fixed size irrespective of the content. `WRT` command will be sent with the data blob ID in the transaction entityId field, public authorization key used to authorize `WRT` and `CLR` commands (subsequent WRT commands to the existing key must use the same key), and the data blob.
|
||||
|
||||
`CLR` command must use with the same entity ID and must be authorized by the same key.
|
||||
|
||||
`READ` command must use the ID which hash would be equal of the ID used to create the data blob, and this ID would also be used as public authorization
|
||||
|
||||
## Algorithm to store and to retrieve data blob.
|
||||
|
||||
**Store data blob**
|
||||
|
||||
- the data blob owner generates X25519 key pair: `(k, pk)`.
|
||||
- private key `pk` will be included in the short link shared with the other party (only base64url encoded key bytes, not X509 encoding).
|
||||
- `HKDF(pk)` will be used to encrypt the link data with secret_box before storing it on the server.
|
||||
- the hash of public key `sha256(k)` will be used as ID by the owner to store and to remove the data blob (`WRT` and `CLR` commands).
|
||||
|
||||
**Retrieve data blob**
|
||||
|
||||
- the sender uses the public key `k` derived from the private key `pk` included in the link as entity ID to retrieve data blob (the server will compute the ID used by the owner as `sha256(k)` and will be able to look it up). This provides the quality that the traffic of the parties has no shared IDs inside TLS. It also means that unlike message queue creation, the ID to retrieve the blob was never sent to the blob creator, and also is not known to the server in advance (the second part is only an observation, in itself it does not increase security, as server has access to an encrypted blob anyway).
|
||||
- note that the sender does not authorize the request to retrieve the blob, as it would not increase security unless a different key is used to authorize, and adding a key would increase link size.
|
||||
- server session keys with the sender will be `(sk, spk)`, where `sk` is public key shared with the sender during session handshake, and `spk` is the private key known only to the server.
|
||||
- this public key `k` will also be combined with server session key `spk` using `dh(k, spk)` to encrypt the response, so that there is no ciphertext in common in sent and received traffic for these blobs. Correlation ID will be used as a nonce for this encryption.
|
||||
- having received the blob, the client can now decrypt it using secret_box with `HKDF(pk)`.
|
||||
|
||||
Using the same key as ID for the request, and also to additionally encrypt the response allows to use a single key in the link, without increasing the link size.
|
||||
|
||||
## Threat model
|
||||
|
||||
**Compromised SMP server**
|
||||
|
||||
can:
|
||||
- delete link data.
|
||||
- hide link selectively from some requests.
|
||||
|
||||
cannot:
|
||||
- undetectably replace link data.
|
||||
- access unencrypted link data, whether it was or was not accessed by the accepting party.
|
||||
- observe IP addresses of the users accessing link data.
|
||||
|
||||
**Passive observer who observed short link**:
|
||||
|
||||
can:
|
||||
- access original unencrypted link data
|
||||
|
||||
cannot:
|
||||
- replace or delete the link data
|
||||
@@ -0,0 +1,81 @@
|
||||
# Storage considerations for SMP queues
|
||||
|
||||
See [Short invitation links](./2024-06-21-short-links.md).
|
||||
|
||||
## Problem
|
||||
|
||||
1) queue records are created permanently, until the clients delete them.
|
||||
|
||||
2) clients only delete queue records based on some user action, pending connections do not expire.
|
||||
|
||||
While part 2 should be improved in the client, indefinite storage of queue records becomes a much bigger issue if each of them would result in a permanent storage of 4-16kb blob in server memory, without server-side expiration for short invitation links.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
1) Add some queue timestamp, e.g. queue creation date, to expire unsecured queues after say 3 weeks.
|
||||
|
||||
The problem with this approach is that contact addresses are also unsecured queues, and they should not be expired.
|
||||
|
||||
We could set really large expiration time, and require that clients "update" the unsecured queues they need at least every 1-2 years, but it would not solve the problem of storing a large number of blobs in the server memory for unused/abandoned 1-time invitations.
|
||||
|
||||
2) Do not store blobs in memory / append-only log, and instead use something like RocksDB. While it may be a correct long term solution, it may be not expedient enough at the current POC stage for this feature. Also, the lack of expiration is wrong in any case and would indefinitely grow server storage.
|
||||
|
||||
3) Add flag allowing the server to differentiate permanent queues used as contact addresses, also using different blob sizes for them. In this case, messaging queues will be expired if not secured after 3 weeks, and contact address queues would be expired if not "updated" by the owner within 2 years.
|
||||
|
||||
Probably all three solutions need to be used, to avoid creating a non-expiring blob storage in memory, as in case too many of such blobs are created it would not be possible to differentiate between real users and resource exhaustion attacks, and unlike with messages, they won't be expiring too.
|
||||
|
||||
Servers already can differentiate messaging queues and contact address queues, if they want to:
|
||||
- with the old 4-message handshake, the confirmation message on a normal queue was different, and also KEY command was eventually used.
|
||||
- with the fast 2-message handshake, while the confirmation message has the same syntax, and the differences are inside encrypted envelope, the client still uses SKEY command.
|
||||
- in both cases, the usual messaging queues are secured, and contact addresses are not, so this difference is visible in the storage as well (although it is not easy to differentiate between abandoned 1-time invitations and contact addresses).
|
||||
|
||||
Differentiating these queues can also allow different message retention times - e.g., the queues for contact addresses could have bigger size, but have lower message retention time.
|
||||
|
||||
## Proposed solution
|
||||
|
||||
1. Add queue updated_at date into queue records. While it adds some metadata, it seems necessary to manage retention and quality of service. It will not include exact time, only date, and the time of creation will be replaced by the time of any update - queue secured, a message is sent, or queue owner subscribes to the queue. To avoid the need to update store log on every message this information can be appended to store log on server termination. Or given that only one update per day is needed it may be ok to make these updates as they happen (temporarily making the sequence and time of these events available in storage).
|
||||
|
||||
2. Add flag to indicate the queue usage - messaging queue or queue for contact address connection requests. This would result in different queue size and different retention policy for queue and its messages. We already have "sender can secure flag" which is, effectively, this flag - contact address queues are never secured. So this does not increase stored metadata in any way.
|
||||
|
||||
## Possible changes to short links
|
||||
|
||||
This is a design considerations and a concept, not a design yet.
|
||||
|
||||
Instead of implementing a generic blob storage that can be used as an attack vector, and adds additional failure point (another server storing blob that is necessary to connect to the queue on the current server), but instead adds an extended queue information blobs, most of which could be dropped without the loss of connectivity, so that the attack can be mitigated by deleting these blobs without users losing the ability to connect, as long as the queue and minimal extended information is retained.
|
||||
|
||||
So, to make the connection there need to be these elements:
|
||||
|
||||
- queue server and queue ID - mandatory part, that can be included in short link
|
||||
- SMP key - mandatory part for all queues. We are considering initializing ratchets earlier for contact addresses, and include ratchet keys and pre-keys into queue data as well, but it is out of scope here.
|
||||
- Ratchet keys - mandatory part for 1-time invitation that won't fit in short link.
|
||||
- PQ key - optional part that can be stored with addresses if ratchet keys are added and with 1-time invitations.
|
||||
- App blobs - chat preferences for 1-time invitation links and profile information for contact addresses.
|
||||
|
||||
So rather that storing one blob with a large address inside it, not associated with the queue, increasing probability of failure and reducing our ability to mitigate resource exhaustion, we could store extended blobs associated with the queues.
|
||||
|
||||
Also, we need the address shared with the sender (party accepting the connection) to be short. We could use a similar approach that was proposed for data blobs, using a single random seed per queues to derive multiple keys and IDs from it. For example:
|
||||
|
||||
1. The queue owner:
|
||||
- generates Ed25529 key pair `(sk, spk)` and X25519 key pair `(dhk, dhpk)` to use with the server, same as now sent in NEW command.
|
||||
- generates queue recipient ID (this ID can still be server-generated).
|
||||
- generates X25519 key pair `(k, pk)` to use with the accepting party.
|
||||
- derives from `k`:
|
||||
- sender ID.
|
||||
- symmetric key for authenticated encryption of blobs.
|
||||
- `k` will be used as short link.
|
||||
2. All other data from the invitation can be included in queue creation request and be associated with the queue as 1-3 blobs with different priority:
|
||||
- ratchet keys - it will have a small size, so only this blob cannot be removed, while other blobs can be removed in case of resource exhaustion.
|
||||
- PQ keys - optional blob.
|
||||
- conversation preferences and profile - can be removed depending on creation time, e.g. all new blobs can be removed.
|
||||
|
||||
The algorithm used to derive key and ID from `k` needs to be cryptographically secure, e.g. it could be some KDF or ChaCha DRG initialized with `k` as seed, TBC.
|
||||
|
||||
So, coupling blob storage with messaging queues has these pros/cons:
|
||||
|
||||
Cons:
|
||||
- no additional layer of privacy - the server used for connection is visible in the link, even after the blobs are removed from the server.
|
||||
|
||||
Pros:
|
||||
- no additional point of failure in the connection process - the same server will be used to retrieve necessary blobs as for connection.
|
||||
- queue blobs of messaging blobs will be automatically removed once the queue is secured or expired, without additional request from the recipient - reducing the storage and the time these blobs are available.
|
||||
- queue blobs for contact addresses will be structured and some of the large blobs can be removed in case of resource exhaustion attack (and recreated by the client if needed), with the only downside that PQ handshake will be postponed (which is the case now) and profile will not be available at a point of connection.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Blob extensions for SMP queues
|
||||
|
||||
Evolution of the design for short links, see [here](./2024-06-21-short-links.md) and [here](./2024-09-05-queue-storage.md).
|
||||
|
||||
## Problems
|
||||
|
||||
Allow storing extended information with SMP queues to improve UX and security of making connections:
|
||||
- short invitation links and contact addresses.
|
||||
- PQ encryption from the first message.
|
||||
- present user profile with chat preferences and welcome message when the public address link is scanned.
|
||||
|
||||
## Design
|
||||
|
||||
1. Queue creation/update date is already added to server persistence, allowing to expire queues and blobs, depending on their usage.
|
||||
2. Add "queue type" metadata to NEW command to indicate whether messaging queue is used as public address or as messaging queue (see previous docs on why it doesn't change threat model). While at the moment it would match sndSecure flag there may be future scenarios when they diverge. Initially only "invitation" and "contact" types will be supported.
|
||||
3. Prohibit sndSecure flag for "contact" queues, prohibit securing contact queues.
|
||||
4. Add "queue blobs" to NEW command:
|
||||
- blob0: ratchetKeys up to N0 bytes - priority 0, can't be removed by the server, only in "invitation"
|
||||
- blob1: PQ key up to N1 bytes - priority 1, can be removed by the server, only used in "invitation"
|
||||
- blob2: Application data up to N2 bytes - priority 2, can be removed by the server.
|
||||
5. Add linkId to NEW command
|
||||
6. linkId and blobs will be removed when queue is secured.
|
||||
7. Add recipient command to remove/upsert blob2 for contact queues.
|
||||
8. Add sender command to retrieve blobs.
|
||||
|
||||
## Protocol
|
||||
|
||||
### Creating a queue:
|
||||
|
||||
The queue owner:
|
||||
- generates Ed25529 key pair `(sk, spk)` and X25519 key pair `(dhk, dhpk)` to use with the server, same as now. `sk` and `dhk` will be sent in NEW command.
|
||||
- generates X25519 key pair `(k, pk)` to use with the accepting party to encrypt queue messages.
|
||||
- derives from `k` using HKDF:
|
||||
- symmetric key `bk` for authenticated encryption of blobs.
|
||||
- `linkId`, will be sent in NEW command.
|
||||
- `k` will be used as short link.
|
||||
- sends NEW command.
|
||||
|
||||
NEW command syntax:
|
||||
|
||||
```abnf
|
||||
create = %s"NEW " linkId queueType recipientAuthPublicKey recipientDhPublicKey
|
||||
basicAuth subscribe sndSecure [ "0" blob0 ] [ "1" blob1 ] [ "2" blob2 ]
|
||||
queueType = %s"I" / %s "C" ; new parameter
|
||||
linkId = length *OCTET ; new parameter,
|
||||
; can be empty in which case blobs won't be allowed
|
||||
blob0 = word16 *OCTET ; new parameter, encrypted ratchet keys,
|
||||
; including nonce and auth tag
|
||||
blob1 = word16 *OCTET ; new parameter, encrypted PQ key
|
||||
blob2 = word16 *OCTET ; new parameter, encrypted application data
|
||||
```
|
||||
|
||||
SET - command to update queue blobs (recipientId is used as entity ID):
|
||||
|
||||
```abnf
|
||||
set = %s"SET " linkId [ "2" blob2 ] ; passing empty blob removes it
|
||||
linkId ; updated (or the same) linkId, can be empty to remove blobs
|
||||
; allows to change the address without removing the queue / changing blobs
|
||||
; (e.g., to avoid losing the messages).
|
||||
```
|
||||
|
||||
### Sending messages to the queue
|
||||
|
||||
GET - command to get queue blobs (linkId is used as entity ID):
|
||||
|
||||
```abnf
|
||||
get = %s"GET"
|
||||
```
|
||||
|
||||
Response to GET:
|
||||
|
||||
```abnf
|
||||
blobs = %s"BLOB" senderId [ "0" blob0 ] [ "1" blob1 ] [ "2" blob2 ]
|
||||
```
|
||||
|
||||
As blobs are retrieved using a separate linkId, once blobs are removed it will be impossible to find senderId from short link - it is a threat model improvement. Once server storage is compacted, it will be impossible to find queue related to the link even with the access to server data (unless server preserves the data).
|
||||
|
||||
### Possible privacy improvement
|
||||
|
||||
We could only allow unauthorized GET and authorized SET commands for long-term "contact" queues, and return BLOB in response to SKEY (or require that GET is authorized) - so that only the person who secures the queue will get access to data blobs. This way it ensures that the parties transmitting the invitation links cannot retrieve their content without the sender noticing it.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Private rendezvous protocol
|
||||
|
||||
## Problem
|
||||
|
||||
Our current handshake protocol is open to this attack: whoever observes the link exchange, knows on which server connection is being made, and if the traffic on this server is observed, then it can confirm communication between parties. Further, even with the [last proposal](./2024-09-09-smp-blobs.md#possible-privacy-improvement), having real-time access to the server data allows to establish the exact messaging queue that is used to send messages.
|
||||
|
||||
## Solution
|
||||
|
||||
We could make the initial link exchange more private by making it harder for any observer to discover which server will be used for messaging by hiding this information from the server that hosts the initial link.
|
||||
|
||||
Preliminary, the protocol could be the following:
|
||||
|
||||
1. Connection initiator stores 224-256 bytes of encrypted connection link on a rendezvous server (link contains server host and linkId on another messaging server, not a rendezvous one).
|
||||
|
||||
2. Rendezvous server adds these links to buckets, up to 64 links per bucket. Bucket ID is the timestamp when the bucket was created + a sequential bucket number, in case more than one bucket is created per second.
|
||||
|
||||
3. The server responds to the link creator with a bucket ID where this link was added. That bucket ID is its timestamp + a number prevents server "fingerprinting" clients and using say one bucket for each client. If timestamp is different or a bucket number within this timestamp is too large, the client can refuse to use it, depending on the client settings.
|
||||
|
||||
4. The initiating party will pass to the accepting party the rendezvous server host, the hash of this bucket ID (bucket link) and the passphrase to derive the key from. The initiating party has an option to pass a link and passphrase via two channels - in which case the link will only contain the bucket ID.
|
||||
|
||||
5. The accepting party would then request the bucket via its ID hash (the server would store hashes to be able to look up - hash is used to prevent showing time in the link) and attempt to decrypt all contained links using the provided key.
|
||||
The accepting party then will continue the connection via the decrypted link.
|
||||
|
||||
This obviously does not protect accepting party from the initiating party, if it can choose rendezvous server it controls. It also does not protect from the malicious rendezvous server that would collaborate with link observers. I think reunion doesn’t protect from it too.
|
||||
|
||||
But it does protect connection from whoever observes the link, particularly if this link only contains the bucket and the key is passed separately, via some other channel.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Sharing protocol ports with HTTPS
|
||||
|
||||
Some networks block all ports other than web ports, including port 5223 used for SMP protocol by default. Running SMP servers on a common web port 443 would allow them to work on more networks. The servers would need to provide an HTTPS page for browsers (and probes).
|
||||
|
||||
## Problem
|
||||
|
||||
Browsers and tools rely on system CA bundles instead of certificate pinning.
|
||||
The crypto parameters used by HTTPS are different from what the protocols use.
|
||||
Public certificate providers like LetsEncrypt can only sign specific types of keys and Ed25519 isn't one of them.
|
||||
|
||||
This means a server should distinguish browser and protocol clients and adjust its behavior to match.
|
||||
|
||||
## Solution
|
||||
|
||||
`tls` package has a server hook that allows producing a different set of `TLS.Credentials` according to a client-provided "Server Name Indication" extension.
|
||||
|
||||
Since LE certificates are only handed out to domain names, TLS client will be sending the SNI.
|
||||
However client transports are constructed over connected sockets and the SNI wouldn't be present unless explicitly requested.
|
||||
When a client sends SNI, then it's a browser and a web credentials should be used.
|
||||
Otherwise it's a protocol client to be offered the self-signed ca, cert and key.
|
||||
|
||||
When a transport colocated with a HTTPS, its ALPN list should be extended with `h2 http/1.1`.
|
||||
The browsers will send it, and it should be checked before running transport client.
|
||||
If HTTP ALPN is detected, then the client connection is served with HTTP `Application` instead (the same "server information" page).
|
||||
|
||||
If some client connects to server IP, doesn't send SNI and doesn't send ALPN, it will look like a pre-handshake client.
|
||||
In that case a server will send its handshake first.
|
||||
This can be mitigated by delaying its handshake and letting the probe to issue its HTTP request.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
An unmodified client should be able to use protocols on port 443 right away.
|
||||
|
||||
The switchover happens inside `runTransportServerState` before `runClient`:
|
||||
|
||||
```haskell
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
-- ...
|
||||
runTransportServerState_ ss started tcpPort serverParams tCfg $ \socket h -> do -- expose raw socket for warp-tls internals to attach
|
||||
negotiated <- getSessionALPN
|
||||
if allowHTTP t && isHTTP negotiated -- only attempt the switch for the TLS transport
|
||||
then runHTTP socket (tlsContext h)-- ... collect data and produce values needed to run WAI Application
|
||||
else runClient serverSignKey t h `runReaderT` env -- performs serverHandshake etc as usual
|
||||
```
|
||||
|
||||
The web app and server live outside, so `runHttp` has to be provided by the `runSMPServer` caller.
|
||||
Additonally, Warp is using its `InternalInfo` object that's scoped to `withII` bracket.
|
||||
|
||||
```haskell
|
||||
runServer ini = do
|
||||
-- ...
|
||||
|
||||
runWebServer ini ServerInformation {config, information} $ if sharedHttps then Nothing else webHttpsParams -- suppress serving https
|
||||
if sharedHttps
|
||||
then withRunHTTP staticFilesPath \attachStatic -> runSMPServer cfg (Just attachStatic) -- provide wrapped application runner
|
||||
else runSMPServer cfg Nothing
|
||||
```
|
||||
|
||||
### Upstream
|
||||
|
||||
The implementation relies on a few modification to upstream code:
|
||||
- `warp-tls`: The library provides `httpOverTls`, but it wants to do handshake itself.
|
||||
Since we have to do the handshake to switch on ALPN, the setup function has to be split.
|
||||
This is a resonable change that may be upstreamed and nothing blocks us from using the recent version.
|
||||
- `warp`: Only the re-export of `serveConnection` is needed.
|
||||
Unfortunately the most recent `warp` version can't be used right away due to dependency cascade around `http-5` and `auto-update-2`.
|
||||
So a fork containing the backported re-export has to be used until the dependencies are refreshed.
|
||||
|
||||
|
||||
### TLS.ServerParams
|
||||
|
||||
When a server has port sharing enabled, a new set of TLS params is loaded and combined with transport params:
|
||||
|
||||
```haskell
|
||||
newEnv config = do
|
||||
-- ...
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
sharedServerParams <- forM ((,) <$> sharedHttpsCredentials config <*> alpn transportConfig) $ \((chain, key), alpn) ->
|
||||
let ca = Nothing -- It is possible to provide CA certificate, but it is typical for web server to use combined certificate chains
|
||||
loadHTTPSServerParams tlsServerParams ca chain key alpn
|
||||
```
|
||||
|
||||
`loadHTTPSServerParams` extends params with:
|
||||
1. `onALPNClientSuggest` hook gets `["h2", "http/1.1"]` added to the ALPN list which is now required.
|
||||
2. `onServerNameIndication` hook added, which upon detecting client SNI prepends the web credentials.
|
||||
3. `sharedCredentials = T.Credentials []` should be done to prevent transport credentials confusing browsers.
|
||||
But that aborts key exchange somewhere in tls internals, so disabled for now.
|
||||
As a workaround, another set of dummy credentials can be provided in the hope that any sane browser would reject them.
|
||||
Like, RC4 ciphers, "impossible" digest combination, etc.
|
||||
|
||||
### supportedParameters
|
||||
|
||||
TLS certificate chains provided by LetsEncrypt use ECDSA/P256 and that requires extending `supportedParameters` with things disabled in transports:
|
||||
|
||||
```haskell
|
||||
browserCiphers =
|
||||
[ TE.cipher_TLS13_AES128CCM8_SHA256
|
||||
, TE.cipher_ECDHE_ECDSA_AES128CCM8_SHA256
|
||||
, TE.cipher_ECDHE_ECDSA_AES256CCM8_SHA256
|
||||
]
|
||||
browserGroups =
|
||||
[ T.P256
|
||||
]
|
||||
browserSigs =
|
||||
[ (T.HashSHA256, T.SignatureECDSA),
|
||||
(T.HashSHA384, T.SignatureECDSA)
|
||||
]
|
||||
```
|
||||
|
||||
This may not be enough for other certificate providers.
|
||||
|
||||
## Configuration
|
||||
|
||||
> XXX: This is for the current implementation and should be updated.
|
||||
|
||||
Web certificate chain is picked up from the WEB section:
|
||||
|
||||
```ini
|
||||
[TRANSPORT]
|
||||
port: 443
|
||||
|
||||
[WEB]
|
||||
https: 443
|
||||
cert: /etc/opt/simplex/web.cert
|
||||
key: /etc/opt/simplex/web.key
|
||||
|
||||
# Alternatively, with a proper access configuration, the paths can point to the LE creds directly:
|
||||
# cert: /etc/letsencrypt/live/smp.hostname.tld/fullchain.pem
|
||||
# key: /etc/letsencrypt/live/smp.hostname.tld/privkey.pem
|
||||
```
|
||||
|
||||
When `TRANSPORT.port` matches `WEB.https` the transport server becomes shared.
|
||||
|
||||
Perhaps a more desirable option would be explicit configuration resulting in additional transported to run:
|
||||
|
||||
```ini
|
||||
[TRANSPORT]
|
||||
port: 5223 ; pure protocol transport
|
||||
# control_port: 5224
|
||||
shared_port: 443 ; variant 1: register in TRANSPORT
|
||||
|
||||
[WEB]
|
||||
https: 443
|
||||
cert: /etc/opt/simplex/web.cert
|
||||
key: /etc/opt/simplex/web.key
|
||||
# transport: on ; variant 2:
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
Serving static files and the protocols togother may pose a problem for those who currently use dedicated web servers as they should switch to embedded http handlers.
|
||||
|
||||
As before, using embedded HTTP server is increasing attack surface.
|
||||
|
||||
Users who want to run everything on a single host will have to add and extra IP address and bind servers to specific IPs instead of 0.0.0.0.
|
||||
An amalgamated server binary can be provided that would contain both SMP and XFTP servers, where transport will dispatch connections by handshake ALPN.
|
||||
|
||||
## Alternative: Use transports routable with reverse-proxies
|
||||
|
||||
An "industrial" reverse proxy may do the ALPN routing, serving HTTP by itself and delegating `smp` and `xftp` to protocol servers.
|
||||
Same with the `websockets`.
|
||||
|
||||
Since this in effect does TLS termination, the protocol servers will have to rely on credentials from protocol handshakes.
|
||||
@@ -1,30 +1,174 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
path_conf_var="/var/opt"
|
||||
path_conf_smp="$path_conf_var/simplex"
|
||||
path_conf_xftp="$path_conf_var/simplex-xftp"
|
||||
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
|
||||
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
|
||||
# Common
|
||||
# ------
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
|
||||
backup_smp() {
|
||||
if [ -e "$path_conf_storelog_smp" ]; then
|
||||
cp "$path_conf_storelog_smp" "${path_conf_storelog_smp}.${date:-date-failed}"
|
||||
fi
|
||||
GRN='\033[0;32m'
|
||||
YLW='\033[1;33m'
|
||||
BLU='\033[1;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
path_conf_var="/var/opt"
|
||||
|
||||
smp_variables() {
|
||||
path_conf_smp="$path_conf_var/simplex"
|
||||
path_conf_smp_archive="$path_conf_smp/backups"
|
||||
path_conf_smp_archive_storelog="$path_conf_smp_archive/queues"
|
||||
path_conf_smp_archive_stats="$path_conf_smp_archive/stats"
|
||||
path_conf_smp_archive_messages="$path_conf_smp_archive/messages"
|
||||
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
|
||||
path_conf_storelog_smp_out="$path_conf_smp_archive_storelog/smp-server-store.log.${date:-date-failed}"
|
||||
|
||||
path_conf_stats_smp="$path_conf_smp/smp-server-stats.log"
|
||||
path_conf_stats_smp_out="$path_conf_smp_archive_stats/smp-server-stats.log.${date:-date-failed}"
|
||||
|
||||
path_conf_messages_smp="$path_conf_smp/smp-server-messages.log"
|
||||
path_conf_messages_smp_out="$path_conf_smp_archive_messages/smp-server-messages.log.${date:-date-failed}"
|
||||
}
|
||||
|
||||
backup_xftp() {
|
||||
if [ -e "$path_conf_storelog_xftp" ]; then
|
||||
cp "$path_conf_storelog_xftp" "${path_conf_storelog_xftp}.${date:-date-failed}"
|
||||
fi
|
||||
xftp_variables() {
|
||||
path_conf_xftp="$path_conf_var/simplex-xftp"
|
||||
path_conf_xftp_archive="$path_conf_xftp/backups"
|
||||
|
||||
path_conf_xftp_archive_storelog="$path_conf_xftp_archive/queues"
|
||||
path_conf_xftp_archive_stats="$path_conf_xftp_archive/stats"
|
||||
|
||||
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
|
||||
path_conf_storelog_xftp_out="$path_conf_xftp_archive_storelog/file-server-store.log.${date:-date-failed}"
|
||||
|
||||
path_conf_stats_xftp="$path_conf_xftp/file-server-stats.log"
|
||||
path_conf_stats_xftp_out="$path_conf_xftp_archive_stats/file-server-stats.log.${date:-date-failed}"
|
||||
}
|
||||
|
||||
if [ "$1" = 'smp-server' ]; then
|
||||
backup_smp
|
||||
elif [ "$1" = 'xftp-server' ]; then
|
||||
backup_xftp
|
||||
else
|
||||
backup_smp
|
||||
backup_xftp
|
||||
fi
|
||||
checks() {
|
||||
result=${SERVICE_RESULT:-exit-code}
|
||||
status=${EXIT_STATUS:-TERM}
|
||||
|
||||
case "$result" in
|
||||
success)
|
||||
case "$status" in
|
||||
TERM)
|
||||
printf "${RED}Refusing to backup files with failed service state${NC}\n"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
:
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
printf "${RED}Refusing to backup files with failed service state${NC}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
smp_check() {
|
||||
if [ ! -d "$path_conf_smp_archive_storelog" ]; then
|
||||
mkdir -p "$path_conf_smp_archive_storelog"
|
||||
fi
|
||||
if [ ! -d "$path_conf_smp_archive_messages" ]; then
|
||||
mkdir -p "$path_conf_smp_archive_messages"
|
||||
fi
|
||||
if [ ! -d "$path_conf_smp_archive_stats" ]; then
|
||||
mkdir -p "$path_conf_smp_archive_stats"
|
||||
fi
|
||||
}
|
||||
|
||||
xftp_check() {
|
||||
if [ ! -d "$path_conf_xftp_archive_storelog" ]; then
|
||||
mkdir -p "$path_conf_xftp_archive_storelog"
|
||||
fi
|
||||
if [ ! -d "$path_conf_xftp_archive_stats" ]; then
|
||||
mkdir -p "$path_conf_xftp_archive_stats"
|
||||
fi
|
||||
}
|
||||
|
||||
backup() {
|
||||
file="$1"
|
||||
out="$2"
|
||||
file_type="$3"
|
||||
|
||||
if [ -e "$file" ]; then
|
||||
if cp "$file" "$out"; then
|
||||
printf "${YLW}${file_type}${NC} ${GRN}backup successful:${NC} ${BLU}%s${NC}\n" "${out}"
|
||||
else
|
||||
printf "${YLW}${file_type}${NC} ${RED}backup failed!${NC}\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
unset file out file_type
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
directory="$1"
|
||||
|
||||
file_type="$2"
|
||||
|
||||
files_date=$(find "$directory" -type f -exec stat --format="%y" {} + | awk '{print $1}' | sort -nr | uniq | awk 'NR==2')
|
||||
|
||||
if [ -n "$files_date" ]; then
|
||||
files=$(find "$directory" -type f -not -newermt "$files_date" -printf "%T@ %Tc %p\n" | sort -n | awk '{print $NF}')
|
||||
|
||||
if [ -n "$files" ]; then
|
||||
printf '%s' "$files" | xargs rm -f
|
||||
printf "${YLW}Old ${file_type} files${NC}${GRN} has been deleted:${NC}\n"
|
||||
files_colored=$(printf '%s' "$files" | awk '{print "\033[1;34m"$0"\033[0m"}')
|
||||
printf "${files_colored}\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
unset directory file_type files_date files
|
||||
}
|
||||
|
||||
smp_backup() {
|
||||
backup "$path_conf_storelog_smp" "$path_conf_storelog_smp_out" 'Storelog'
|
||||
backup "$path_conf_messages_smp" "$path_conf_messages_smp_out" 'Messages'
|
||||
backup "$path_conf_stats_smp" "$path_conf_stats_smp_out" 'Stats'
|
||||
}
|
||||
|
||||
smp_cleanup() {
|
||||
cleanup "$path_conf_smp_archive_storelog" 'storelog'
|
||||
cleanup "$path_conf_smp_archive_stats" 'stats'
|
||||
cleanup "$path_conf_smp_archive_messages" 'messages'
|
||||
}
|
||||
|
||||
xftp_backup() {
|
||||
backup "$path_conf_storelog_xftp" "$path_conf_storelog_xftp_out" 'Storelog'
|
||||
backup "$path_conf_stats_xftp" "$path_conf_stats_xftp_out" 'Stats'
|
||||
}
|
||||
|
||||
xftp_cleanup() {
|
||||
cleanup "$path_conf_xftp_archive_storelog" 'storelog'
|
||||
cleanup "$path_conf_xftp_archive_stats" 'stats'
|
||||
}
|
||||
|
||||
main() {
|
||||
type="${1:-}"
|
||||
|
||||
checks
|
||||
|
||||
case "$type" in
|
||||
smp-server)
|
||||
smp_variables
|
||||
smp_check
|
||||
smp_backup
|
||||
smp_cleanup
|
||||
;;
|
||||
xftp-server)
|
||||
xftp_variables
|
||||
xftp_check
|
||||
xftp_backup
|
||||
xftp_cleanup
|
||||
;;
|
||||
*)
|
||||
printf "${YLW}Unknown server type.${NC}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+17
-3
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 6.0.3.0
|
||||
version: 6.1.0.4
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -135,6 +135,7 @@ library
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
@@ -419,9 +420,10 @@ executable smp-server
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, wai
|
||||
, wai-app-static
|
||||
, warp
|
||||
, warp-tls
|
||||
, warp ==3.3.30
|
||||
, warp-tls ==3.4.7
|
||||
, websockets ==0.12.*
|
||||
, yaml ==0.11.*
|
||||
, zstd ==0.1.3.*
|
||||
@@ -610,6 +612,7 @@ test-suite simplexmq-test
|
||||
CoreTests.CryptoTests
|
||||
CoreTests.EncodingTests
|
||||
CoreTests.RetryIntervalTests
|
||||
CoreTests.SOCKSSettings
|
||||
CoreTests.TRcvQueuesTests
|
||||
CoreTests.UtilTests
|
||||
CoreTests.VersionRangeTests
|
||||
@@ -626,9 +629,12 @@ test-suite simplexmq-test
|
||||
XFTPCLI
|
||||
XFTPClient
|
||||
XFTPServerTests
|
||||
Static
|
||||
Static.Embedded
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
tests
|
||||
apps/smp-server/web
|
||||
default-extensions:
|
||||
StrictData
|
||||
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
|
||||
@@ -656,12 +662,15 @@ test-suite simplexmq-test
|
||||
, deepseq ==1.4.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hashable ==1.4.*
|
||||
, hourglass ==0.2.*
|
||||
, hspec ==2.11.*
|
||||
, hspec-core ==2.11.*
|
||||
, http-client
|
||||
, http-client-tls
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
@@ -691,6 +700,11 @@ test-suite simplexmq-test
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, unordered-containers
|
||||
, wai
|
||||
, wai-app-static
|
||||
, warp
|
||||
, warp-tls
|
||||
, websockets ==0.12.*
|
||||
, yaml ==0.11.*
|
||||
, zstd ==0.1.3.*
|
||||
|
||||
@@ -22,6 +22,7 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
@@ -36,8 +37,8 @@ import Simplex.Messaging.Client
|
||||
TransportSession,
|
||||
chooseTransportHost,
|
||||
defaultNetworkConfig,
|
||||
proxyUsername,
|
||||
transportClientConfig,
|
||||
clientSocksCredentials,
|
||||
unexpectedResponse,
|
||||
)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -52,7 +53,7 @@ import Simplex.Messaging.Protocol
|
||||
SenderId,
|
||||
pattern NoEntity,
|
||||
)
|
||||
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
|
||||
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), defaultSupportedParams)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
@@ -98,17 +99,17 @@ defaultXFTPClientConfig =
|
||||
clientALPN = Just supportedXFTPhandshakes
|
||||
}
|
||||
|
||||
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
|
||||
let username = proxyUsername transportSession
|
||||
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> UTCTime -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} proxySessTs disconnected = runExceptT $ do
|
||||
let socksCreds = clientSocksCredentials xftpNetworkConfig proxySessTs transportSession
|
||||
ProtocolServer _ host port keyHash = srv
|
||||
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
|
||||
let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN}
|
||||
let tcConfig = (transportClientConfig xftpNetworkConfig useHost True) {alpn = clientALPN}
|
||||
http2Config = xftpHTTP2Config tcConfig config
|
||||
clientVar <- newTVarIO Nothing
|
||||
let usePort = if null port then "443" else port
|
||||
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
|
||||
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
|
||||
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client socksCreds useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
|
||||
let HTTP2Client {sessionId, sessionALPN} = http2Client
|
||||
v = VersionXFTP 1
|
||||
thServerVRange = versionToRange v
|
||||
@@ -172,7 +173,7 @@ xftpHTTP2Config :: TransportClientConfig -> XFTPClientConfig -> HTTP2ClientConfi
|
||||
xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} =
|
||||
defaultHTTP2ClientConfig
|
||||
{ bodyHeadSize = xftpBlockSize,
|
||||
suportedTLSParams = supportedParameters,
|
||||
suportedTLSParams = defaultSupportedParams,
|
||||
connTimeout = tcpConnectTimeout,
|
||||
transportConfig
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError)
|
||||
@@ -30,6 +31,7 @@ type XFTPClientVar = TMVar (Either XFTPClientAgentError XFTPClient)
|
||||
|
||||
data XFTPClientAgent = XFTPClientAgent
|
||||
{ xftpClients :: TMap XFTPServer XFTPClientVar,
|
||||
startedAt :: UTCTime,
|
||||
config :: XFTPClientAgentConfig
|
||||
}
|
||||
|
||||
@@ -56,19 +58,20 @@ data XFTPClientAgentError = XFTPClientAgentError XFTPServer XFTPClientError
|
||||
newXFTPAgent :: XFTPClientAgentConfig -> IO XFTPClientAgent
|
||||
newXFTPAgent config = do
|
||||
xftpClients <- TM.emptyIO
|
||||
pure XFTPClientAgent {xftpClients, config}
|
||||
startedAt <- getCurrentTime
|
||||
pure XFTPClientAgent {xftpClients, startedAt, config}
|
||||
|
||||
type ME a = ExceptT XFTPClientAgentError IO a
|
||||
|
||||
getXFTPServerClient :: XFTPClientAgent -> XFTPServer -> ME XFTPClient
|
||||
getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
|
||||
getXFTPServerClient XFTPClientAgent {xftpClients, startedAt, config} srv = do
|
||||
atomically getClientVar >>= either newXFTPClient waitForXFTPClient
|
||||
where
|
||||
connectClient :: ME XFTPClient
|
||||
connectClient =
|
||||
ExceptT $
|
||||
first (XFTPClientAgentError srv)
|
||||
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) clientDisconnected
|
||||
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) startedAt clientDisconnected
|
||||
|
||||
clientDisconnected :: XFTPClient -> IO ()
|
||||
clientDisconnected _ = do
|
||||
|
||||
@@ -33,7 +33,6 @@ 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
|
||||
@@ -57,15 +56,16 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (CorrId (..), EntityId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth, pattern NoEntity)
|
||||
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdAuthorization)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, getRoundedSystemTime)
|
||||
import Simplex.Messaging.Server.Stats
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport (ALPN, SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..), defaultSupportedParams)
|
||||
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 (runLocalTCPServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Transport.Server (runLocalTCPServer)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import System.Exit (exitFailure)
|
||||
@@ -91,32 +91,31 @@ data XFTPTransportRequest = XFTPTransportRequest
|
||||
runXFTPServer :: XFTPServerConfig -> IO ()
|
||||
runXFTPServer cfg = do
|
||||
started <- newEmptyTMVarIO
|
||||
runXFTPServerBlocking started cfg
|
||||
runXFTPServerBlocking started cfg $ Just supportedXFTPhandshakes
|
||||
|
||||
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> IO ()
|
||||
runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started)
|
||||
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> Maybe [ALPN] -> IO ()
|
||||
runXFTPServerBlocking started cfg alpn_ = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started alpn_)
|
||||
|
||||
data Handshake
|
||||
= HandshakeSent C.PrivateKeyX25519
|
||||
| HandshakeAccepted (THandleParams XFTPVersion 'TServer)
|
||||
|
||||
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do
|
||||
xftpServer :: XFTPServerConfig -> TMVar Bool -> Maybe [ALPN] -> M ()
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started alpn_ = 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
|
||||
srvCreds@(chain, pk) <- asks tlsServerCreds
|
||||
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
|
||||
sessions <- liftIO TM.emptyIO
|
||||
let cleanup sessionId = atomically $ TM.delete sessionId sessions
|
||||
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
|
||||
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize defaultSupportedParams srvCreds alpn_ transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r xftpBlockSize
|
||||
let v = VersionXFTP 1
|
||||
thServerVRange = versionToRange v
|
||||
@@ -399,7 +398,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
r <- runExceptT $ do
|
||||
sizes <- asks $ allowedChunkSizes . config
|
||||
unless (size file `elem` sizes) $ throwE SIZE
|
||||
ts <- liftIO getSystemTime
|
||||
ts <- liftIO getFileTime
|
||||
-- TODO validate body empty
|
||||
sId <- ExceptT $ addFileRetry st file 3 ts
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
@@ -412,7 +411,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
|
||||
pure $ FRSndIds sId rIds
|
||||
pure $ either FRErr id r
|
||||
addFileRetry :: FileStore -> FileInfo -> Int -> SystemTime -> M (Either XFTPErrorType XFTPFileId)
|
||||
addFileRetry :: FileStore -> FileInfo -> Int -> RoundedSystemTime -> M (Either XFTPErrorType XFTPFileId)
|
||||
addFileRetry st file n ts =
|
||||
retryAdd n $ \sId -> runExceptT $ do
|
||||
ExceptT $ addFile st sId file ts
|
||||
@@ -531,6 +530,9 @@ deleteServerFile_ FileRec {senderId, fileInfo, filePath} = do
|
||||
liftIO $ atomicModifyIORef'_ (filesCount stats) (subtract 1)
|
||||
liftIO $ atomicModifyIORef'_ (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
|
||||
|
||||
getFileTime :: IO RoundedSystemTime
|
||||
getFileTime = getRoundedSystemTime fileTimePrecision
|
||||
|
||||
expireServerFiles :: Maybe Int -> ExpirationConfig -> M ()
|
||||
expireServerFiles itemDelay expCfg = do
|
||||
st <- asks store
|
||||
|
||||
@@ -29,7 +29,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (ALPN)
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), loadFingerprint, loadServerCredential)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import System.IO (IOMode (..))
|
||||
import UnliftIO.STM
|
||||
@@ -57,10 +57,7 @@ data XFTPServerConfig = XFTPServerConfig
|
||||
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
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
xftpCredentials :: ServerCredentials,
|
||||
-- | XFTP client-server protocol version range
|
||||
xftpServerVRange :: VersionRangeXFTP,
|
||||
-- stats config - see SMP server config
|
||||
@@ -85,7 +82,7 @@ data XFTPEnv = XFTPEnv
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
random :: TVar ChaChaDRG,
|
||||
serverIdentity :: C.KeyHash,
|
||||
tlsServerParams :: T.ServerParams,
|
||||
tlsServerCreds :: T.Credential,
|
||||
serverStats :: FileServerStats
|
||||
}
|
||||
|
||||
@@ -103,7 +100,7 @@ supportedXFTPhandshakes :: [ALPN]
|
||||
supportedXFTPhandshakes = ["xftp/1"]
|
||||
|
||||
newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv
|
||||
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
|
||||
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, xftpCredentials} = do
|
||||
random <- C.newRandom
|
||||
store <- newFileStore
|
||||
storeLog <- mapM (`readWriteFileStore` store) storeLogFile
|
||||
@@ -112,10 +109,10 @@ newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertifi
|
||||
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 <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
tlsServerCreds <- loadServerCredential xftpCredentials
|
||||
Fingerprint fp <- loadFingerprint xftpCredentials
|
||||
serverStats <- newFileServerStats =<< getCurrentTime
|
||||
pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
|
||||
pure XFTPEnv {config, store, storeLog, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
|
||||
|
||||
countUsedStorage :: M.Map k FileRec -> Int64
|
||||
countUsedStorage = M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0
|
||||
|
||||
@@ -19,7 +19,7 @@ import Options.Applicative
|
||||
import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Description (FileSize (..))
|
||||
import Simplex.FileTransfer.Server (runXFTPServer)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration)
|
||||
import Simplex.FileTransfer.Transport (supportedFileServerVRange)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -28,7 +28,7 @@ import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
import System.FilePath (combine)
|
||||
@@ -173,9 +173,12 @@ xftpServerCLI cfgPath logPath = do
|
||||
{ ttl = readStrictIni "INACTIVE_CLIENTS" "ttl" ini,
|
||||
checkInterval = readStrictIni "INACTIVE_CLIENTS" "check_interval" ini
|
||||
},
|
||||
caCertificateFile = c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile,
|
||||
xftpCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just $ c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile
|
||||
},
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
logStatsInterval = logStats $> 86400, -- seconds
|
||||
logStatsStartTime = 0, -- seconds from 00:00 UTC
|
||||
@@ -183,8 +186,7 @@ xftpServerCLI cfgPath logPath = do
|
||||
serverStatsBackupFile = logStats $> combine logPath "file-server-stats.log",
|
||||
transportConfig =
|
||||
defaultTransportServerConfig
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
|
||||
alpn = Just supportedXFTPhandshakes
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
|
||||
},
|
||||
responseDelay = 0
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ module Simplex.FileTransfer.Server.Store
|
||||
expiredFilePath,
|
||||
getFile,
|
||||
ackFile,
|
||||
fileTimePrecision,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -25,12 +26,12 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Int (Int64)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Time.Clock.System (SystemTime (..))
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..), SFileParty (..), XFTPFileId)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime (..))
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (ifM, ($>>=))
|
||||
@@ -46,10 +47,14 @@ data FileRec = FileRec
|
||||
fileInfo :: FileInfo,
|
||||
filePath :: TVar (Maybe FilePath),
|
||||
recipientIds :: TVar (Set RecipientId),
|
||||
createdAt :: SystemTime
|
||||
createdAt :: RoundedSystemTime
|
||||
}
|
||||
|
||||
fileTimePrecision :: Int64
|
||||
fileTimePrecision = 3600 -- truncate creation time to 1 hour
|
||||
|
||||
data FileRecipient = FileRecipient RecipientId RcvPublicAuthKey
|
||||
deriving (Show)
|
||||
|
||||
instance StrEncoding FileRecipient where
|
||||
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
|
||||
@@ -62,14 +67,14 @@ newFileStore = do
|
||||
usedStorage <- newTVarIO 0
|
||||
pure FileStore {files, recipients, usedStorage}
|
||||
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> SystemTime -> STM (Either XFTPErrorType ())
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> RoundedSystemTime -> STM (Either XFTPErrorType ())
|
||||
addFile FileStore {files} sId fileInfo createdAt =
|
||||
ifM (TM.member sId files) (pure $ Left DUPLICATE_) $ do
|
||||
f <- newFileRec sId fileInfo createdAt
|
||||
TM.insert sId f files
|
||||
pure $ Right ()
|
||||
|
||||
newFileRec :: SenderId -> FileInfo -> SystemTime -> STM FileRec
|
||||
newFileRec :: SenderId -> FileInfo -> RoundedSystemTime -> STM FileRec
|
||||
newFileRec senderId fileInfo createdAt = do
|
||||
recipientIds <- newTVar S.empty
|
||||
filePath <- newTVar Nothing
|
||||
@@ -120,8 +125,8 @@ getFile st party fId = case party of
|
||||
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe (Maybe FilePath))
|
||||
expiredFilePath FileStore {files} sId old =
|
||||
TM.lookup sId files
|
||||
$>>= \FileRec {filePath, createdAt} ->
|
||||
if systemSeconds createdAt < old
|
||||
$>>= \FileRec {filePath, createdAt = RoundedSystemTime createdAt} ->
|
||||
if createdAt + fileTimePrecision < old
|
||||
then Just <$> readTVar filePath
|
||||
else pure Nothing
|
||||
|
||||
|
||||
@@ -28,22 +28,23 @@ 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.Time.Clock.System (SystemTime)
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime)
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (bshow, whenM)
|
||||
import System.Directory (doesFileExist, renameFile)
|
||||
import System.IO
|
||||
|
||||
data FileStoreLogRecord
|
||||
= AddFile SenderId FileInfo SystemTime
|
||||
= AddFile SenderId FileInfo RoundedSystemTime
|
||||
| PutFile SenderId FilePath
|
||||
| AddRecipients SenderId (NonEmpty FileRecipient)
|
||||
| DeleteFile SenderId
|
||||
| AckFile RecipientId
|
||||
deriving (Show)
|
||||
|
||||
instance StrEncoding FileStoreLogRecord where
|
||||
strEncode = \case
|
||||
@@ -64,7 +65,7 @@ instance StrEncoding FileStoreLogRecord where
|
||||
logFileStoreRecord :: StoreLog 'WriteMode -> FileStoreLogRecord -> IO ()
|
||||
logFileStoreRecord = writeStoreLogRecord
|
||||
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> SystemTime -> IO ()
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> RoundedSystemTime -> IO ()
|
||||
logAddFile s = logFileStoreRecord s .:. AddFile
|
||||
|
||||
logPutFile :: StoreLog 'WriteMode -> SenderId -> FilePath -> IO ()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
@@ -166,7 +167,7 @@ import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client (ProtocolClient (..), SMPClientError, ServerTransmission (..), ServerTransmissionBatch, temporaryClientError, unexpectedResponse)
|
||||
import Simplex.Messaging.Client (SMPClientError, ServerTransmission (..), ServerTransmissionBatch, temporaryClientError, unexpectedResponse)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs)
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
||||
@@ -181,7 +182,7 @@ import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), ErrorType (AUTH), MsgBod
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (SMPVersion, THandleParams (sessionId))
|
||||
import Simplex.Messaging.Transport (SMPVersion)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.RemoteControl.Client
|
||||
@@ -473,14 +474,14 @@ testProtocolServer c userId srv = withAgentEnv' c $ case protocolTypeI @p of
|
||||
|
||||
-- | set SOCKS5 proxy on/off and optionally set TCP timeouts for fast network
|
||||
setNetworkConfig :: AgentClient -> NetworkConfig -> IO ()
|
||||
setNetworkConfig c@AgentClient {useNetworkConfig} cfg' = do
|
||||
changed <- atomically $ do
|
||||
setNetworkConfig c@AgentClient {useNetworkConfig, proxySessTs} cfg' = do
|
||||
(spChanged, changed) <- atomically $ do
|
||||
(_, cfg) <- readTVar useNetworkConfig
|
||||
if cfg == cfg'
|
||||
then pure False
|
||||
else
|
||||
let cfgSlow = slowNetworkConfig cfg'
|
||||
in True <$ (cfgSlow `seq` writeTVar useNetworkConfig (cfgSlow, cfg'))
|
||||
let changed = cfg /= cfg'
|
||||
!cfgSlow = slowNetworkConfig cfg'
|
||||
when changed $ writeTVar useNetworkConfig (cfgSlow, cfg')
|
||||
pure (socksProxy cfg /= socksProxy cfg', changed)
|
||||
when spChanged $ getCurrentTime >>= atomically . writeTVar proxySessTs
|
||||
when changed $ reconnectAllServers c
|
||||
|
||||
setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO ()
|
||||
@@ -778,7 +779,7 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv
|
||||
lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId
|
||||
when enableNtfs $ do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCCreate)
|
||||
atomically $ sendNtfSubCommand ns (NSCCreate, [connId])
|
||||
let crData = ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData
|
||||
case cMode of
|
||||
SCMContact -> pure (connId, CRContactUri crData)
|
||||
@@ -923,7 +924,7 @@ createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVers
|
||||
lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId
|
||||
when enableNtfs $ do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCCreate)
|
||||
atomically $ sendNtfSubCommand ns (NSCCreate, [connId])
|
||||
pure qInfo
|
||||
|
||||
-- | Approve confirmation (LET command) in Reader monad
|
||||
@@ -1013,14 +1014,13 @@ subscribeConnections' c connIds = do
|
||||
order _ = 4
|
||||
sendNtfCreate :: NtfSupervisor -> Map ConnId (Either AgentErrorType ()) -> Map ConnId SomeConn -> AM' ()
|
||||
sendNtfCreate ns rcvRs cs = do
|
||||
-- TODO this needs to be batched end to end.
|
||||
-- Currently, the only change is to ignore failed subscriptions.
|
||||
let oks = M.keysSet $ M.filter (either temporaryAgentError $ const True) rcvRs
|
||||
forM_ (M.restrictKeys cs oks) $ \case
|
||||
SomeConn _ conn -> do
|
||||
let cmd = if enableNtfs $ toConnData conn then NSCCreate else NSCDelete
|
||||
ConnData {connId} = toConnData conn
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, cmd)
|
||||
cs' = M.restrictKeys cs oks
|
||||
(csCreate, csDelete) = M.partition (\(SomeConn _ conn) -> enableNtfs $ toConnData conn) cs'
|
||||
sendNtfCmd NSCCreate csCreate
|
||||
sendNtfCmd NSCSmpDelete csDelete
|
||||
where
|
||||
sendNtfCmd cmd cs' = forM_ (L.nonEmpty $ M.keys cs') $ \cids -> atomically $ writeTBQueue (ntfSubQ ns) (cmd, cids)
|
||||
resumeDelivery :: Map ConnId SomeConn -> AM ()
|
||||
resumeDelivery conns = do
|
||||
conns' <- M.restrictKeys conns . S.fromList <$> withStore' c getConnectionsForDelivery
|
||||
@@ -1259,7 +1259,7 @@ runCommandProcessing c@AgentClient {subQ} connId server_ Worker {doWork} = do
|
||||
withStore' c $ \db -> deleteConnRcvQueue db rq'
|
||||
when (enableNtfs cData) $ do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCCreate)
|
||||
atomically $ sendNtfSubCommand ns (NSCCreate, [connId])
|
||||
let conn' = DuplexConnection cData (rq'' :| rqs') sqs
|
||||
notify $ SWITCH QDRcv SPCompleted $ connectionStats conn'
|
||||
_ -> internalErr "ICQDelete: cannot delete the only queue in connection"
|
||||
@@ -1716,12 +1716,6 @@ connRcvQueues = \case
|
||||
SndConnection _ _ -> []
|
||||
NewConnection _ -> []
|
||||
|
||||
disableConn :: AgentClient -> ConnId -> AM' ()
|
||||
disableConn c connId = do
|
||||
atomically $ removeSubscription c connId
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCDelete)
|
||||
|
||||
-- Unlike deleteConnectionsAsync, this function does not mark connections as deleted in case of deletion failure.
|
||||
deleteConnections' :: AgentClient -> [ConnId] -> AM (Map ConnId (Either AgentErrorType ()))
|
||||
deleteConnections' = deleteConnections_ getConns False False
|
||||
@@ -1748,7 +1742,7 @@ prepareDeleteConnections_ getConnections c waitDelivery connIds = do
|
||||
(delRs, rcvQs) = M.mapEither rcvQueues cs
|
||||
rqs = concat $ M.elems rcvQs
|
||||
connIds' = M.keys rcvQs
|
||||
lift . forM_ connIds' $ disableConn c
|
||||
lift $ forM_ (L.nonEmpty connIds') unsubConnIds
|
||||
-- ! delRs is not used to notify about the result in any of the calling functions,
|
||||
-- ! it is only used to check results count in deleteConnections_;
|
||||
-- ! if it was used to notify about the result, it might be necessary to differentiate
|
||||
@@ -1762,6 +1756,12 @@ prepareDeleteConnections_ getConnections c waitDelivery connIds = do
|
||||
rcvQueues (SomeConn _ conn) = case connRcvQueues conn of
|
||||
[] -> Left $ Right ()
|
||||
rqs -> Right rqs
|
||||
unsubConnIds :: NonEmpty ConnId -> AM' ()
|
||||
unsubConnIds connIds' = do
|
||||
forM_ connIds' $ \connId ->
|
||||
atomically $ removeSubscription c connId
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (NSCDeleteSub, connIds')
|
||||
notify = atomically . writeTBQueue (subQ c)
|
||||
|
||||
deleteConnQueues :: AgentClient -> Bool -> Bool -> [RcvQueue] -> AM' (Map ConnId (Either AgentErrorType ()))
|
||||
@@ -1902,16 +1902,12 @@ registerNtfToken' c suppliedDeviceToken suppliedNtfMode =
|
||||
cron <- asks $ ntfCron . config
|
||||
agentNtfEnableCron c tknId tkn cron
|
||||
when (suppliedNtfMode == NMInstant) $ initializeNtfSubs c
|
||||
when (suppliedNtfMode == NMPeriodic && savedNtfMode == NMInstant) $ deleteNtfSubs c NSCDelete
|
||||
when (suppliedNtfMode == NMPeriodic && savedNtfMode == NMInstant) $ deleteNtfSubs c NSCSmpDelete
|
||||
-- possible improvement: get updated token status from the server, or maybe TCRON could return the current status
|
||||
pure ntfTknStatus
|
||||
| otherwise -> replaceToken tknId
|
||||
(Just tknId, Just NTADelete) -> do
|
||||
agentNtfDeleteToken c tknId tkn
|
||||
withStore' c (`removeNtfToken` tkn)
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ nsRemoveNtfToken ns
|
||||
pure NTExpired
|
||||
-- deprecated
|
||||
(Just _tknId, Just NTADelete) -> deleteToken c tkn $> NTExpired
|
||||
_ -> pure ntfTknStatus
|
||||
withStore' c $ \db -> updateNtfMode db tkn suppliedNtfMode
|
||||
pure status
|
||||
@@ -1984,8 +1980,8 @@ deleteNtfToken' :: AgentClient -> DeviceToken -> AM ()
|
||||
deleteNtfToken' c deviceToken =
|
||||
withStore' c getSavedNtfToken >>= \case
|
||||
Just tkn@NtfToken {deviceToken = savedDeviceToken} -> do
|
||||
when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "deleteNtfToken: different token"
|
||||
deleteToken_ c tkn
|
||||
when (deviceToken /= savedDeviceToken) $ logWarn "deleteNtfToken: different token"
|
||||
deleteToken c tkn
|
||||
deleteNtfSubs c NSCSmpDelete
|
||||
_ -> throwE $ CMD PROHIBITED "deleteNtfToken: no token"
|
||||
|
||||
@@ -2017,21 +2013,8 @@ toggleConnectionNtfs' c connId enable = do
|
||||
| otherwise = do
|
||||
withStore' c $ \db -> setConnectionNtfs db connId enable
|
||||
ns <- asks ntfSupervisor
|
||||
let cmd = if enable then NSCCreate else NSCDelete
|
||||
atomically $ sendNtfSubCommand ns (connId, cmd)
|
||||
|
||||
deleteToken_ :: AgentClient -> NtfToken -> AM ()
|
||||
deleteToken_ c tkn@NtfToken {ntfTokenId, ntfTknStatus} = do
|
||||
ns <- asks ntfSupervisor
|
||||
forM_ ntfTokenId $ \tknId -> do
|
||||
let ntfTknAction = Just NTADelete
|
||||
withStore' c $ \db -> updateNtfToken db tkn ntfTknStatus ntfTknAction
|
||||
atomically $ nsUpdateToken ns tkn {ntfTknStatus, ntfTknAction}
|
||||
agentNtfDeleteToken c tknId tkn `catchAgentError` \case
|
||||
NTF _ AUTH -> pure ()
|
||||
e -> throwE e
|
||||
withStore' c $ \db -> removeNtfToken db tkn
|
||||
atomically $ nsRemoveNtfToken ns
|
||||
let cmd = if enable then NSCCreate else NSCSmpDelete
|
||||
atomically $ sendNtfSubCommand ns (cmd, [connId])
|
||||
|
||||
withToken :: AgentClient -> NtfToken -> Maybe (NtfTknStatus, NtfTknAction) -> (NtfTknStatus, Maybe NtfTknAction) -> AM a -> AM NtfTknStatus
|
||||
withToken c tkn@NtfToken {deviceToken, ntfMode} from_ (toStatus, toAction_) f = do
|
||||
@@ -2065,13 +2048,24 @@ deleteNtfSubs c deleteCmd = do
|
||||
sendNtfConnCommands :: AgentClient -> NtfSupervisorCommand -> AM ()
|
||||
sendNtfConnCommands c cmd = do
|
||||
ns <- asks ntfSupervisor
|
||||
connIds <- liftIO $ getSubscriptions c
|
||||
forM_ connIds $ \connId -> do
|
||||
withStore' c (`getConnData` connId) >>= \case
|
||||
Just (ConnData {enableNtfs}, _) ->
|
||||
when enableNtfs . atomically $ writeTBQueue (ntfSubQ ns) (connId, cmd)
|
||||
_ ->
|
||||
atomically $ writeTBQueue (subQ c) ("", connId, AEvt SAEConn $ ERR $ INTERNAL "no connection data")
|
||||
connIds <- liftIO $ S.toList <$> getSubscriptions c
|
||||
rs <- lift $ withStoreBatch' c (\db -> map (getConnData db) connIds)
|
||||
let (connIds', cErrs) = enabledNtfConns (zip connIds rs)
|
||||
forM_ (L.nonEmpty connIds') $ \connIds'' ->
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (cmd, connIds'')
|
||||
unless (null cErrs) $ atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ ERRS cErrs)
|
||||
where
|
||||
enabledNtfConns :: [(ConnId, Either AgentErrorType (Maybe (ConnData, ConnectionMode)))] -> ([ConnId], [(ConnId, AgentErrorType)])
|
||||
enabledNtfConns = foldr addEnabledConn ([], [])
|
||||
where
|
||||
addEnabledConn ::
|
||||
(ConnId, Either AgentErrorType (Maybe (ConnData, ConnectionMode))) ->
|
||||
([ConnId], [(ConnId, AgentErrorType)]) ->
|
||||
([ConnId], [(ConnId, AgentErrorType)])
|
||||
addEnabledConn cData_ (cIds, errs) = case cData_ of
|
||||
(_, Right (Just (ConnData {connId, enableNtfs}, _))) -> if enableNtfs then (connId : cIds, errs) else (cIds, errs)
|
||||
(connId, Right Nothing) -> (cIds, (connId, INTERNAL "no connection data") : errs)
|
||||
(connId, Left e) -> (cIds, (connId, e) : errs)
|
||||
|
||||
setNtfServers :: AgentClient -> [NtfServer] -> IO ()
|
||||
setNtfServers c = atomically . writeTVar (ntfServers c)
|
||||
@@ -2152,6 +2146,7 @@ cleanupManager c@AgentClient {subQ} = do
|
||||
run ERR $ withStore' c (`deleteRcvMsgHashesExpired` ttl)
|
||||
run ERR $ withStore' c (`deleteSndMsgsExpired` ttl)
|
||||
run ERR $ withStore' c (`deleteRatchetKeyHashesExpired` ttl)
|
||||
run ERR $ withStore' c (`deleteExpiredNtfTokensToDelete` ttl)
|
||||
run RFERR deleteRcvFilesExpired
|
||||
run RFERR deleteRcvFilesDeleted
|
||||
run RFERR deleteRcvFilesTmpPaths
|
||||
@@ -2450,17 +2445,14 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), _v, sessId
|
||||
handleNotifyAck :: AM ACKd -> AM ACKd
|
||||
handleNotifyAck m = m `catchAgentError` \e -> notify (ERR e) >> ack
|
||||
SMP.END ->
|
||||
atomically (TM.lookup tSess (smpClients c) $>>= (tryReadTMVar . sessionVar) >>= processEND)
|
||||
atomically (ifM (activeClientSession c tSess sessId) (removeSubscription c connId $> True) (pure False))
|
||||
>>= notifyEnd
|
||||
where
|
||||
processEND = \case
|
||||
Just (Right clnt)
|
||||
| sessId == sessionId (thParams $ connectedClient clnt) ->
|
||||
removeSubscription c connId $> True
|
||||
_ -> pure False
|
||||
notifyEnd removed
|
||||
| removed = notify END >> logServer "<--" c srv rId "END"
|
||||
| otherwise = logServer "<--" c srv rId "END from disconnected client - ignored"
|
||||
-- Possibly, we need to add some flag to connection that it was deleted
|
||||
SMP.DELD -> atomically (removeSubscription c connId) >> notify DELD
|
||||
SMP.ERR e -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e
|
||||
r -> unexpected r
|
||||
where
|
||||
|
||||
@@ -56,8 +56,10 @@ module Simplex.Messaging.Agent.Client
|
||||
secureQueue,
|
||||
secureSndQueue,
|
||||
enableQueueNotifications,
|
||||
EnableQueueNtfReq (..),
|
||||
enableQueuesNtfs,
|
||||
disableQueueNotifications,
|
||||
DisableQueueNtfReq,
|
||||
disableQueuesNtfs,
|
||||
sendAgentMessage,
|
||||
getQueueInfo,
|
||||
@@ -68,7 +70,9 @@ module Simplex.Messaging.Agent.Client
|
||||
agentNtfDeleteToken,
|
||||
agentNtfEnableCron,
|
||||
agentNtfCreateSubscription,
|
||||
agentNtfCreateSubscriptions,
|
||||
agentNtfCheckSubscription,
|
||||
agentNtfCheckSubscriptions,
|
||||
agentNtfDeleteSubscription,
|
||||
agentXFTPDownloadChunk,
|
||||
agentXFTPNewChunk,
|
||||
@@ -115,6 +119,7 @@ module Simplex.Messaging.Agent.Client
|
||||
hasWorkToDo,
|
||||
hasWorkToDo',
|
||||
withWork,
|
||||
withWorkItems,
|
||||
agentOperations,
|
||||
agentOperationBracket,
|
||||
waitUntilActive,
|
||||
@@ -150,6 +155,7 @@ module Simplex.Messaging.Agent.Client
|
||||
incXFTPServerStat',
|
||||
incXFTPServerSizeStat,
|
||||
incNtfServerStat,
|
||||
incNtfServerStat',
|
||||
AgentWorkersDetails (..),
|
||||
getAgentWorkersDetails,
|
||||
AgentWorkersSummary (..),
|
||||
@@ -185,7 +191,7 @@ import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (isRight, partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (deleteFirstsBy, foldl', partition, (\\))
|
||||
import Data.List (deleteFirstsBy, find, foldl', partition, (\\))
|
||||
import Data.List.NonEmpty (NonEmpty (..), (<|))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
@@ -256,8 +262,8 @@ import Simplex.Messaging.Protocol
|
||||
VersionSMPC,
|
||||
XFTPServer,
|
||||
XFTPServerWithAuth,
|
||||
pattern NoEntity,
|
||||
sameSrvAddr',
|
||||
pattern NoEntity,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
@@ -333,6 +339,7 @@ data AgentClient = AgentClient
|
||||
smpSubWorkers :: TMap SMPTransportSession (SessionVar (Async ())),
|
||||
clientId :: Int,
|
||||
agentEnv :: Env,
|
||||
proxySessTs :: TVar UTCTime,
|
||||
smpServersStats :: TMap (UserId, SMPServer) AgentSMPServerStats,
|
||||
xftpServersStats :: TMap (UserId, XFTPServer) AgentXFTPServerStats,
|
||||
ntfServersStats :: TMap (UserId, NtfServer) AgentNtfServerStats,
|
||||
@@ -461,6 +468,7 @@ newAgentClient :: Int -> InitialAgentServers -> UTCTime -> Env -> IO AgentClient
|
||||
newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs agentEnv = do
|
||||
let cfg = config agentEnv
|
||||
qSize = tbqSize cfg
|
||||
proxySessTs <- newTVarIO =<< getCurrentTime
|
||||
acThread <- newTVarIO Nothing
|
||||
active <- newTVarIO True
|
||||
subQ <- newTBQueueIO qSize
|
||||
@@ -533,6 +541,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs a
|
||||
smpSubWorkers,
|
||||
clientId,
|
||||
agentEnv,
|
||||
proxySessTs,
|
||||
smpServersStats,
|
||||
xftpServersStats,
|
||||
ntfServersStats,
|
||||
@@ -657,7 +666,7 @@ getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq
|
||||
Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT
|
||||
|
||||
smpConnectClient :: AgentClient -> SMPTransportSession -> TMap SMPServer ProxiedRelayVar -> SMPClientVar -> AM SMPConnectedClient
|
||||
smpConnectClient c@AgentClient {smpClients, msgQ} tSess@(_, srv, _) prs v =
|
||||
smpConnectClient c@AgentClient {smpClients, msgQ, proxySessTs} tSess@(_, srv, _) prs v =
|
||||
newProtocolClient c tSess smpClients connectClient v
|
||||
`catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwE e
|
||||
where
|
||||
@@ -667,7 +676,8 @@ smpConnectClient c@AgentClient {smpClients, msgQ} tSess@(_, srv, _) prs v =
|
||||
g <- asks random
|
||||
env <- ask
|
||||
liftError (protocolClientError SMP $ B.unpack $ strEncode srv) $ do
|
||||
smp <- ExceptT $ getProtocolClient g tSess cfg (Just msgQ) $ smpClientDisconnected c tSess env v' prs
|
||||
ts <- readTVarIO proxySessTs
|
||||
smp <- ExceptT $ getProtocolClient g tSess cfg (Just msgQ) ts $ smpClientDisconnected c tSess env v' prs
|
||||
pure SMPConnectedClient {connectedClient = smp, proxiedRelays = prs}
|
||||
|
||||
smpClientDisconnected :: AgentClient -> SMPTransportSession -> Env -> SMPClientVar -> TMap SMPServer ProxiedRelayVar -> SMPClient -> IO ()
|
||||
@@ -756,7 +766,7 @@ reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do
|
||||
notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, AEvt (sAEntity @e) cmd)
|
||||
|
||||
getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient
|
||||
getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(_, srv, _) = do
|
||||
getNtfServerClient c@AgentClient {active, ntfClients, workerSeq, proxySessTs} tSess@(_, srv, _) = do
|
||||
unlessM (readTVarIO active) $ throwE INACTIVE
|
||||
ts <- liftIO getCurrentTime
|
||||
atomically (getSessVar workerSeq tSess ntfClients ts)
|
||||
@@ -768,8 +778,9 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(_, srv,
|
||||
connectClient v = do
|
||||
cfg <- lift $ getClientConfig c ntfCfg
|
||||
g <- asks random
|
||||
ts <- readTVarIO proxySessTs
|
||||
liftError' (protocolClientError NTF $ B.unpack $ strEncode srv) $
|
||||
getProtocolClient g tSess cfg Nothing $
|
||||
getProtocolClient g tSess cfg Nothing ts $
|
||||
clientDisconnected v
|
||||
|
||||
clientDisconnected :: NtfClientVar -> NtfClient -> IO ()
|
||||
@@ -779,7 +790,7 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(_, srv,
|
||||
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
||||
|
||||
getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient
|
||||
getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(_, srv, _) = do
|
||||
getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq, proxySessTs} tSess@(_, srv, _) = do
|
||||
unlessM (readTVarIO active) $ throwE INACTIVE
|
||||
ts <- liftIO getCurrentTime
|
||||
atomically (getSessVar workerSeq tSess xftpClients ts)
|
||||
@@ -791,8 +802,9 @@ getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(_, srv
|
||||
connectClient v = do
|
||||
cfg <- asks $ xftpCfg . config
|
||||
xftpNetworkConfig <- getNetworkConfig c
|
||||
ts <- readTVarIO proxySessTs
|
||||
liftError' (protocolClientError XFTP $ B.unpack $ strEncode srv) $
|
||||
X.getXFTPClient tSess cfg {xftpNetworkConfig} $
|
||||
X.getXFTPClient tSess cfg {xftpNetworkConfig} ts $
|
||||
clientDisconnected v
|
||||
|
||||
clientDisconnected :: XFTPClientVar -> XFTPClient -> IO ()
|
||||
@@ -1199,7 +1211,8 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do
|
||||
g <- asks random
|
||||
liftIO $ do
|
||||
let tSess = (userId, srv, Nothing)
|
||||
getProtocolClient g tSess cfg Nothing (\_ -> pure ()) >>= \case
|
||||
ts <- readTVarIO $ proxySessTs c
|
||||
getProtocolClient g tSess cfg Nothing ts (\_ -> pure ()) >>= \case
|
||||
Right smp -> do
|
||||
rKeys@(_, rpKey) <- atomically $ C.generateAuthKeyPair ra g
|
||||
(sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g
|
||||
@@ -1229,7 +1242,8 @@ runXFTPServerTest c userId (ProtoServerWithAuth srv auth) = do
|
||||
rcvPath <- getTempFilePath workDir
|
||||
liftIO $ do
|
||||
let tSess = (userId, srv, Nothing)
|
||||
X.getXFTPClient tSess cfg {xftpNetworkConfig} (\_ -> pure ()) >>= \case
|
||||
ts <- readTVarIO $ proxySessTs c
|
||||
X.getXFTPClient tSess cfg {xftpNetworkConfig} ts (\_ -> pure ()) >>= \case
|
||||
Right xftp -> withTestChunk filePath $ do
|
||||
(sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(rcvKey, rpKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
@@ -1273,7 +1287,8 @@ runNTFServerTest c userId (ProtoServerWithAuth srv _) = do
|
||||
g <- asks random
|
||||
liftIO $ do
|
||||
let tSess = (userId, srv, Nothing)
|
||||
getProtocolClient g tSess cfg Nothing (\_ -> pure ()) >>= \case
|
||||
ts <- readTVarIO $ proxySessTs c
|
||||
getProtocolClient g tSess cfg Nothing ts (\_ -> pure ()) >>= \case
|
||||
Right ntf -> do
|
||||
(nKey, npKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
(dhKey, _) <- atomically $ C.generateKeyPair g
|
||||
@@ -1410,7 +1425,7 @@ subscribeQueues c qs = do
|
||||
checkQueue rq = do
|
||||
prohibited <- liftIO $ hasGetLock c rq
|
||||
pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED "subscribeQueues") else Right rq
|
||||
subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ())
|
||||
subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses RcvQueue SMPClientError ())
|
||||
subscribeQueues_ env session smp qs' = do
|
||||
let (userId, srv, _) = transportSession' smp
|
||||
atomically $ incSMPServerStat' c userId srv connSubAttempts $ length qs'
|
||||
@@ -1441,31 +1456,33 @@ activeClientSession c tSess sessId = sameSess <$> tryReadSessVar tSess (smpClien
|
||||
Just (Right (SMPConnectedClient smp _)) -> sessId == sessionId (thParams smp)
|
||||
_ -> False
|
||||
|
||||
type BatchResponses e r = NonEmpty (RcvQueue, Either e r)
|
||||
type BatchResponses q e r = NonEmpty (q, Either e r)
|
||||
|
||||
sendTSessionBatches :: forall q r. ByteString -> (q -> RcvQueue) -> (SMPClient -> NonEmpty q -> IO (BatchResponses SMPClientError r)) -> AgentClient -> [q] -> AM' [(RcvQueue, Either AgentErrorType r)]
|
||||
-- Please note: this function does not preserve order of results to be the same as the order of arguments,
|
||||
-- it includes arguments in the results instead.
|
||||
sendTSessionBatches :: forall q r. ByteString -> (q -> RcvQueue) -> (SMPClient -> NonEmpty q -> IO (BatchResponses q SMPClientError r)) -> AgentClient -> [q] -> AM' [(q, Either AgentErrorType r)]
|
||||
sendTSessionBatches statCmd toRQ action c qs =
|
||||
concatMap L.toList <$> (mapConcurrently sendClientBatch =<< batchQueues)
|
||||
where
|
||||
batchQueues :: AM' [(SMPTransportSession, NonEmpty q)]
|
||||
batchQueues = do
|
||||
mode <- getSessionMode c
|
||||
pure . M.assocs $ foldl' (batch mode) M.empty qs
|
||||
pure . M.assocs $ foldr (batch mode) M.empty qs
|
||||
where
|
||||
batch mode m q =
|
||||
batch mode q m =
|
||||
let tSess = mkSMPTSession (toRQ q) mode
|
||||
in M.alter (Just . maybe [q] (q <|)) tSess m
|
||||
sendClientBatch :: (SMPTransportSession, NonEmpty q) -> AM' (BatchResponses AgentErrorType r)
|
||||
sendClientBatch :: (SMPTransportSession, NonEmpty q) -> AM' (BatchResponses q AgentErrorType r)
|
||||
sendClientBatch (tSess@(_, srv, _), qs') =
|
||||
tryAgentError' (getSMPServerClient c tSess) >>= \case
|
||||
Left e -> pure $ L.map ((,Left e) . toRQ) qs'
|
||||
Left e -> pure $ L.map (,Left e) qs'
|
||||
Right (SMPConnectedClient smp _) -> liftIO $ do
|
||||
logServer' "-->" c srv (bshow (length qs') <> " queues") statCmd
|
||||
L.map agentError <$> action smp qs'
|
||||
where
|
||||
agentError = second . first $ protocolClientError SMP $ clientServer smp
|
||||
|
||||
sendBatch :: (SMPClient -> NonEmpty (SMP.RcvPrivateAuthKey, SMP.RecipientId) -> IO (NonEmpty (Either SMPClientError ()))) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ())
|
||||
sendBatch :: (SMPClient -> NonEmpty (SMP.RcvPrivateAuthKey, SMP.RecipientId) -> IO (NonEmpty (Either SMPClientError ()))) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses RcvQueue SMPClientError ())
|
||||
sendBatch smpCmdFunc smp qs = L.zip qs <$> smpCmdFunc smp (L.map queueCreds qs)
|
||||
where
|
||||
queueCreds RcvQueue {rcvPrivateKey, rcvId} = (rcvPrivateKey, rcvId)
|
||||
@@ -1594,22 +1611,39 @@ enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtf
|
||||
withSMPClient c rq "NKEY <nkey>" $ \smp ->
|
||||
enableSMPQueueNotifications smp rcvPrivateKey rcvId notifierKey rcvNtfPublicDhKey
|
||||
|
||||
enableQueuesNtfs :: AgentClient -> [(RcvQueue, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey)] -> AM' [(RcvQueue, Either AgentErrorType (SMP.NotifierId, SMP.RcvNtfPublicDhKey))]
|
||||
enableQueuesNtfs = sendTSessionBatches "NKEY" fst3 enableQueues_
|
||||
data EnableQueueNtfReq = EnableQueueNtfReq
|
||||
{ eqnrNtfSub :: NtfSubscription,
|
||||
eqnrRq :: RcvQueue,
|
||||
eqnrAuthKeyPair :: C.AAuthKeyPair,
|
||||
eqnrRcvKeyPair :: C.KeyPairX25519
|
||||
}
|
||||
|
||||
enableQueuesNtfs :: AgentClient -> [EnableQueueNtfReq] -> AM' [(EnableQueueNtfReq, Either AgentErrorType (SMP.NotifierId, SMP.RcvNtfPublicDhKey))]
|
||||
enableQueuesNtfs = sendTSessionBatches "NKEY" eqnrRq enableQueues_
|
||||
where
|
||||
fst3 (x, _, _) = x
|
||||
enableQueues_ :: SMPClient -> NonEmpty (RcvQueue, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey) -> IO (NonEmpty (RcvQueue, Either (ProtocolClientError ErrorType) (SMP.NotifierId, RcvNtfPublicDhKey)))
|
||||
enableQueues_ smp qs' = L.zipWith ((,) . fst3) qs' <$> enableSMPQueuesNtfs smp (L.map queueCreds qs')
|
||||
queueCreds :: (RcvQueue, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey) -> (SMP.RcvPrivateAuthKey, SMP.RecipientId, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey)
|
||||
queueCreds (RcvQueue {rcvPrivateKey, rcvId}, notifierKey, rcvNtfPublicDhKey) = (rcvPrivateKey, rcvId, notifierKey, rcvNtfPublicDhKey)
|
||||
enableQueues_ :: SMPClient -> NonEmpty EnableQueueNtfReq -> IO (NonEmpty (EnableQueueNtfReq, Either (ProtocolClientError ErrorType) (SMP.NotifierId, RcvNtfPublicDhKey)))
|
||||
enableQueues_ smp qs' = L.zip qs' <$> enableSMPQueuesNtfs smp (L.map queueCreds qs')
|
||||
queueCreds :: EnableQueueNtfReq -> (SMP.RcvPrivateAuthKey, SMP.RecipientId, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey)
|
||||
queueCreds EnableQueueNtfReq {eqnrRq, eqnrAuthKeyPair, eqnrRcvKeyPair} =
|
||||
let RcvQueue {rcvPrivateKey, rcvId} = eqnrRq
|
||||
(ntfPublicKey, _) = eqnrAuthKeyPair
|
||||
(rcvNtfPubDhKey, _) = eqnrRcvKeyPair
|
||||
in (rcvPrivateKey, rcvId, ntfPublicKey, rcvNtfPubDhKey)
|
||||
|
||||
disableQueueNotifications :: AgentClient -> RcvQueue -> AM ()
|
||||
disableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} =
|
||||
withSMPClient c rq "NDEL" $ \smp ->
|
||||
disableSMPQueueNotifications smp rcvPrivateKey rcvId
|
||||
|
||||
disableQueuesNtfs :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())]
|
||||
disableQueuesNtfs = sendTSessionBatches "NDEL" id $ sendBatch disableSMPQueuesNtfs
|
||||
type DisableQueueNtfReq = (NtfSubscription, RcvQueue)
|
||||
|
||||
disableQueuesNtfs :: AgentClient -> [DisableQueueNtfReq] -> AM' [(DisableQueueNtfReq, Either AgentErrorType ())]
|
||||
disableQueuesNtfs = sendTSessionBatches "NDEL" snd disableQueues_
|
||||
where
|
||||
disableQueues_ :: SMPClient -> NonEmpty DisableQueueNtfReq -> IO (NonEmpty (DisableQueueNtfReq, Either (ProtocolClientError ErrorType) ()))
|
||||
disableQueues_ smp qs' = L.zip qs' <$> disableSMPQueuesNtfs smp (L.map queueCreds qs')
|
||||
queueCreds :: DisableQueueNtfReq -> (SMP.RcvPrivateAuthKey, SMP.RecipientId)
|
||||
queueCreds (_, RcvQueue {rcvPrivateKey, rcvId}) = (rcvPrivateKey, rcvId)
|
||||
|
||||
sendAck :: AgentClient -> RcvQueue -> MsgId -> AM ()
|
||||
sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId = do
|
||||
@@ -1687,8 +1721,8 @@ agentNtfReplaceToken :: AgentClient -> NtfTokenId -> NtfToken -> DeviceToken ->
|
||||
agentNtfReplaceToken c tknId NtfToken {ntfServer, ntfPrivKey} token =
|
||||
withNtfClient c ntfServer tknId "TRPL" $ \ntf -> ntfReplaceToken ntf ntfPrivKey tknId token
|
||||
|
||||
agentNtfDeleteToken :: AgentClient -> NtfTokenId -> NtfToken -> AM ()
|
||||
agentNtfDeleteToken c tknId NtfToken {ntfServer, ntfPrivKey} =
|
||||
agentNtfDeleteToken :: AgentClient -> NtfServer -> C.APrivateAuthKey -> NtfTokenId -> AM ()
|
||||
agentNtfDeleteToken c ntfServer ntfPrivKey tknId =
|
||||
withNtfClient c ntfServer tknId "TDEL" $ \ntf -> ntfDeleteToken ntf ntfPrivKey tknId
|
||||
|
||||
agentNtfEnableCron :: AgentClient -> NtfTokenId -> NtfToken -> Word16 -> AM ()
|
||||
@@ -1699,10 +1733,34 @@ agentNtfCreateSubscription :: AgentClient -> NtfTokenId -> NtfToken -> SMPQueueN
|
||||
agentNtfCreateSubscription c tknId NtfToken {ntfServer, ntfPrivKey} smpQueue nKey =
|
||||
withNtfClient c ntfServer tknId "SNEW" $ \ntf -> ntfCreateSubscription ntf ntfPrivKey (NewNtfSub tknId smpQueue nKey)
|
||||
|
||||
agentNtfCheckSubscription :: AgentClient -> NtfSubscriptionId -> NtfToken -> AM NtfSubStatus
|
||||
agentNtfCheckSubscription c subId NtfToken {ntfServer, ntfPrivKey} =
|
||||
agentNtfCreateSubscriptions :: AgentClient -> NtfToken -> NonEmpty (NewNtfEntity 'Subscription) -> AM' (NonEmpty (Either AgentErrorType NtfSubscriptionId))
|
||||
agentNtfCreateSubscriptions = withNtfBatch "SNEW" ntfCreateSubscriptions
|
||||
|
||||
agentNtfCheckSubscription :: AgentClient -> NtfToken -> NtfSubscriptionId -> AM NtfSubStatus
|
||||
agentNtfCheckSubscription c NtfToken {ntfServer, ntfPrivKey} subId =
|
||||
withNtfClient c ntfServer subId "SCHK" $ \ntf -> ntfCheckSubscription ntf ntfPrivKey subId
|
||||
|
||||
agentNtfCheckSubscriptions :: AgentClient -> NtfToken -> NonEmpty NtfSubscriptionId -> AM' (NonEmpty (Either AgentErrorType NtfSubStatus))
|
||||
agentNtfCheckSubscriptions = withNtfBatch "SCHK" ntfCheckSubscriptions
|
||||
|
||||
-- This batch sends all commands to one ntf server (client can only use one server at a time)
|
||||
withNtfBatch ::
|
||||
ByteString ->
|
||||
(NtfClient -> C.APrivateAuthKey -> NonEmpty a -> IO (NonEmpty (Either NtfClientError r))) ->
|
||||
AgentClient ->
|
||||
NtfToken ->
|
||||
NonEmpty a ->
|
||||
AM' (NonEmpty (Either AgentErrorType r))
|
||||
withNtfBatch cmdStr action c NtfToken {ntfServer, ntfPrivKey} subs = do
|
||||
let tSess = (0, ntfServer, Nothing)
|
||||
tryAgentError' (getNtfServerClient c tSess) >>= \case
|
||||
Left e -> pure $ L.map (\_ -> Left e) subs
|
||||
Right ntf -> liftIO $ do
|
||||
logServer' "-->" c ntfServer (bshow (length subs) <> " subscriptions") cmdStr
|
||||
L.map agentError <$> action ntf ntfPrivKey subs
|
||||
where
|
||||
agentError = first $ protocolClientError NTF $ clientServer ntf
|
||||
|
||||
agentNtfDeleteSubscription :: AgentClient -> NtfSubscriptionId -> NtfToken -> AM ()
|
||||
agentNtfDeleteSubscription c subId NtfToken {ntfServer, ntfPrivKey} =
|
||||
withNtfClient c ntfServer subId "SDEL" $ \ntf -> ntfDeleteSubscription ntf ntfPrivKey subId
|
||||
@@ -1800,12 +1858,39 @@ withWork c doWork getWork action =
|
||||
withStore' c getWork >>= \case
|
||||
Right (Just r) -> action r
|
||||
Right Nothing -> noWork
|
||||
-- worker is stopped here (noWork) because the next iteration is likely to produce the same result
|
||||
Left e@SEWorkItemError {} -> noWork >> notifyErr (CRITICAL False) e
|
||||
Left e -> notifyErr INTERNAL e
|
||||
where
|
||||
noWork = liftIO $ noWorkToDo doWork
|
||||
notifyErr err e = atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e)
|
||||
|
||||
withWorkItems :: AgentClient -> TMVar () -> (DB.Connection -> IO (Either StoreError [Either StoreError a])) -> (NonEmpty a -> AM ()) -> AM ()
|
||||
withWorkItems c doWork getWork action = do
|
||||
withStore' c getWork >>= \case
|
||||
Right [] -> noWork
|
||||
Right rs -> do
|
||||
let (errs, items) = partitionEithers rs
|
||||
case L.nonEmpty items of
|
||||
Just items' -> action items'
|
||||
Nothing -> do
|
||||
let criticalErr = find workItemError errs
|
||||
forM_ criticalErr $ \err -> do
|
||||
notifyErr (CRITICAL False) err
|
||||
when (all workItemError errs) noWork
|
||||
unless (null errs) $
|
||||
atomically $
|
||||
writeTBQueue (subQ c) ("", "", AEvt SAENone $ ERRS $ map (\e -> ("", INTERNAL $ show e)) errs)
|
||||
Left e
|
||||
| workItemError e -> noWork >> notifyErr (CRITICAL False) e
|
||||
| otherwise -> notifyErr INTERNAL e
|
||||
where
|
||||
workItemError = \case
|
||||
SEWorkItemError {} -> True
|
||||
_ -> False
|
||||
noWork = liftIO $ noWorkToDo doWork
|
||||
notifyErr err e = atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e)
|
||||
|
||||
noWorkToDo :: TMVar () -> IO ()
|
||||
noWorkToDo = void . atomically . tryTakeTMVar
|
||||
{-# INLINE noWorkToDo #-}
|
||||
@@ -2001,9 +2086,13 @@ incXFTPServerStat_ = incServerStat (\AgentClient {xftpServersStats = s} -> s) ne
|
||||
{-# INLINE incXFTPServerStat_ #-}
|
||||
|
||||
incNtfServerStat :: AgentClient -> UserId -> NtfServer -> (AgentNtfServerStats -> TVar Int) -> STM ()
|
||||
incNtfServerStat c userId srv sel = incServerStat (\AgentClient {ntfServersStats = s} -> s) newAgentNtfServerStats c userId srv sel 1
|
||||
incNtfServerStat c userId srv sel = incNtfServerStat' c userId srv sel 1
|
||||
{-# INLINE incNtfServerStat #-}
|
||||
|
||||
incNtfServerStat' :: AgentClient -> UserId -> NtfServer -> (AgentNtfServerStats -> TVar Int) -> Int -> STM ()
|
||||
incNtfServerStat' = incServerStat (\AgentClient {ntfServersStats = s} -> s) newAgentNtfServerStats
|
||||
{-# INLINE incNtfServerStat' #-}
|
||||
|
||||
incServerStat :: Num n => (AgentClient -> TMap (UserId, ProtocolServer p) s) -> STM s -> AgentClient -> UserId -> ProtocolServer p -> (s -> TVar n) -> n -> STM ()
|
||||
incServerStat statsSel mkNewStats c userId srv sel n = do
|
||||
TM.lookup (userId, srv) (statsSel c) >>= \case
|
||||
|
||||
@@ -74,8 +74,7 @@ import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth, ProtocolServer, ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange)
|
||||
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.Transport (SMPVersion)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors')
|
||||
import System.Mem.Weak (Weak)
|
||||
import System.Random (StdGen, newStdGen)
|
||||
@@ -150,6 +149,8 @@ data AgentConfig = AgentConfig
|
||||
xftpMaxRecipientsPerRequest :: Int,
|
||||
deleteErrorCount :: Int,
|
||||
ntfCron :: Word16,
|
||||
ntfBatchSize :: Int,
|
||||
ntfSubFirstCheckInterval :: NominalDiffTime,
|
||||
ntfSubCheckInterval :: NominalDiffTime,
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
@@ -193,9 +194,9 @@ defaultAgentConfig =
|
||||
rcvAuthAlg = C.AuthAlg C.SEd25519, -- this will stay as Ed25519
|
||||
sndAuthAlg = C.AuthAlg C.SEd25519, -- TODO replace with X25519 when switching to v7
|
||||
connIdBytes = 12,
|
||||
tbqSize = 64,
|
||||
smpCfg = defaultSMPClientConfig {defaultTransport = (show defaultSMPPort, transport @TLS)},
|
||||
ntfCfg = defaultNTFClientConfig {defaultTransport = ("443", transport @TLS)},
|
||||
tbqSize = 128,
|
||||
smpCfg = defaultSMPClientConfig,
|
||||
ntfCfg = defaultNTFClientConfig,
|
||||
xftpCfg = defaultXFTPClientConfig,
|
||||
reconnectInterval = defaultReconnectInterval,
|
||||
messageRetryInterval = defaultMessageRetryInterval,
|
||||
@@ -219,7 +220,9 @@ defaultAgentConfig =
|
||||
xftpMaxRecipientsPerRequest = 200,
|
||||
deleteErrorCount = 10,
|
||||
ntfCron = 20, -- minutes
|
||||
ntfSubCheckInterval = nominalDay,
|
||||
ntfBatchSize = 150,
|
||||
ntfSubFirstCheckInterval = nominalDay,
|
||||
ntfSubCheckInterval = 3 * nominalDay,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
-- ! we do not generate these
|
||||
caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
|
||||
@@ -254,12 +257,13 @@ createAgentStore dbFilePath dbKey keepKey = createSQLiteStore dbFilePath dbKey k
|
||||
|
||||
data NtfSupervisor = NtfSupervisor
|
||||
{ ntfTkn :: TVar (Maybe NtfToken),
|
||||
ntfSubQ :: TBQueue (ConnId, NtfSupervisorCommand),
|
||||
ntfSubQ :: TBQueue (NtfSupervisorCommand, NonEmpty ConnId),
|
||||
ntfWorkers :: TMap NtfServer Worker,
|
||||
ntfSMPWorkers :: TMap SMPServer Worker
|
||||
ntfSMPWorkers :: TMap SMPServer Worker,
|
||||
ntfTknDelWorkers :: TMap NtfServer Worker
|
||||
}
|
||||
|
||||
data NtfSupervisorCommand = NSCCreate | NSCDelete | NSCSmpDelete | NSCNtfWorker NtfServer | NSCNtfSMPWorker SMPServer
|
||||
data NtfSupervisorCommand = NSCCreate | NSCSmpDelete | NSCDeleteSub
|
||||
deriving (Show)
|
||||
|
||||
newNtfSubSupervisor :: Natural -> IO NtfSupervisor
|
||||
@@ -268,7 +272,8 @@ newNtfSubSupervisor qSize = do
|
||||
ntfSubQ <- newTBQueueIO qSize
|
||||
ntfWorkers <- TM.emptyIO
|
||||
ntfSMPWorkers <- TM.emptyIO
|
||||
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers}
|
||||
ntfTknDelWorkers <- TM.emptyIO
|
||||
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers, ntfTknDelWorkers}
|
||||
|
||||
data XFTPAgent = XFTPAgent
|
||||
{ -- if set, XFTP file paths will be considered as relative to this directory
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
@@ -13,6 +15,7 @@ module Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
nsRemoveNtfToken,
|
||||
sendNtfSubCommand,
|
||||
instantNotifications,
|
||||
deleteToken,
|
||||
closeNtfSupervisor,
|
||||
getNtfServer,
|
||||
)
|
||||
@@ -22,23 +25,32 @@ import Control.Logger.Simple (logError, logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Either (fromRight, partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.List (foldl')
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import Data.Maybe (catMaybes)
|
||||
import qualified Data.Set as S
|
||||
import Data.Time (UTCTime, addUTCTime, getCurrentTime)
|
||||
import Data.Time.Clock (diffUTCTime)
|
||||
import Simplex.Messaging.Agent.Client
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol (AEvent (..), AEvt (..), AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..), SAEntity (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Stats
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol (NtfSubStatus (..), NtfTknStatus (..), SMPQueueNtf (..))
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Protocol (NtfServer, SMPServer, sameSrvAddr)
|
||||
import Simplex.Messaging.Util (diffToMicroseconds, threadDelay', tshow, unlessM)
|
||||
import Simplex.Messaging.Protocol (NtfServer, sameSrvAddr)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Util (diffToMicroseconds, threadDelay', tshow)
|
||||
import System.Random (randomR)
|
||||
import UnliftIO
|
||||
import UnliftIO.Concurrent (forkIO)
|
||||
@@ -47,91 +59,134 @@ import qualified UnliftIO.Exception as E
|
||||
runNtfSupervisor :: AgentClient -> AM' ()
|
||||
runNtfSupervisor c = do
|
||||
ns <- asks ntfSupervisor
|
||||
runExceptT startTknDelete >>= \case
|
||||
Left e -> notifyErr e
|
||||
Right _ -> pure ()
|
||||
forever $ do
|
||||
cmd@(connId, _) <- atomically . readTBQueue $ ntfSubQ ns
|
||||
handleErr connId . agentOperationBracket c AONtfNetwork waitUntilActive $
|
||||
runExceptT (processNtfSub c cmd) >>= \case
|
||||
Left e -> notifyErr connId e
|
||||
cmd <- atomically . readTBQueue $ ntfSubQ ns
|
||||
handleErr . agentOperationBracket c AONtfNetwork waitUntilActive $
|
||||
runExceptT (processNtfCmd c cmd) >>= \case
|
||||
Left e -> notifyErr e
|
||||
Right _ -> return ()
|
||||
where
|
||||
handleErr :: ConnId -> AM' () -> AM' ()
|
||||
handleErr connId = E.handle $ \(e :: E.SomeException) -> do
|
||||
startTknDelete :: AM ()
|
||||
startTknDelete = do
|
||||
pendingDelServers <- withStore' c getPendingDelTknServers
|
||||
lift . forM_ pendingDelServers $ getNtfTknDelWorker True c
|
||||
handleErr :: AM' () -> AM' ()
|
||||
handleErr = E.handle $ \(e :: E.SomeException) -> do
|
||||
logError $ "runNtfSupervisor error " <> tshow e
|
||||
notifyErr connId e
|
||||
notifyErr connId e = notifyInternalError c connId $ "runNtfSupervisor error " <> show e
|
||||
notifyErr e
|
||||
notifyErr e = notifyInternalError' c $ "runNtfSupervisor error " <> show e
|
||||
|
||||
processNtfSub :: AgentClient -> (ConnId, NtfSupervisorCommand) -> AM ()
|
||||
processNtfSub c (connId, cmd) = do
|
||||
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
|
||||
partitionErrs :: (a -> ConnId) -> [a] -> [Either AgentErrorType b] -> ([(ConnId, AgentErrorType)], [b])
|
||||
partitionErrs f xs = partitionEithers . zipWith (\x -> first (f x,)) xs
|
||||
{-# INLINE partitionErrs #-}
|
||||
|
||||
ntfSubConnId :: NtfSubscription -> ConnId
|
||||
ntfSubConnId NtfSubscription {connId} = connId
|
||||
|
||||
processNtfCmd :: AgentClient -> (NtfSupervisorCommand, NonEmpty ConnId) -> AM ()
|
||||
processNtfCmd c (cmd, connIds) = do
|
||||
logInfo $ "processNtfCmd - cmd = " <> tshow cmd
|
||||
let connIds' = L.toList connIds
|
||||
case cmd of
|
||||
NSCCreate -> do
|
||||
(a, RcvQueue {userId, server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
|
||||
a <- liftIO $ getNtfSubscription db connId
|
||||
q <- ExceptT $ getPrimaryRcvQueue db connId
|
||||
pure (a, q)
|
||||
logInfo $ "processNtfSub, NSCCreate - a = " <> tshow a
|
||||
case a of
|
||||
Nothing -> do
|
||||
(cErrs, rqSubActions) <- lift $ partitionErrs id connIds' <$> withStoreBatch c (\db -> map (getQueueSub db) connIds')
|
||||
notifyErrs c cErrs
|
||||
logInfo $ "processNtfCmd, NSCCreate - length rqSubs = " <> tshow (length rqSubActions)
|
||||
let (ns, rs, css, cns) = partitionQueueSubActions rqSubActions
|
||||
createNewSubs ns
|
||||
resetSubs rs
|
||||
lift $ do
|
||||
mapM_ (getNtfSMPWorker True c) (S.fromList css)
|
||||
mapM_ (getNtfNTFWorker True c) (S.fromList cns)
|
||||
where
|
||||
getQueueSub ::
|
||||
DB.Connection ->
|
||||
ConnId ->
|
||||
IO (Either AgentErrorType (RcvQueue, Maybe NtfSupervisorSub))
|
||||
getQueueSub db connId = fmap (first storeError) $ runExceptT $ do
|
||||
rq <- ExceptT $ getPrimaryRcvQueue db connId
|
||||
sub <- liftIO $ getNtfSubscription db connId
|
||||
pure (rq, sub)
|
||||
createNewSubs :: [RcvQueue] -> AM ()
|
||||
createNewSubs rqs = do
|
||||
withTokenServer $ \ntfServer -> do
|
||||
case clientNtfCreds of
|
||||
Just ClientNtfCreds {notifierId} -> do
|
||||
let newSub = newNtfSubscription userId connId smpServer (Just notifierId) ntfServer NASKey
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NSANtf NSACreate
|
||||
lift . void $ getNtfNTFWorker True c ntfServer
|
||||
Nothing -> do
|
||||
let newSub = newNtfSubscription userId connId smpServer Nothing ntfServer NASNew
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NSASMP NSASmpKey
|
||||
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')
|
||||
| sameSrvAddr smpServer smpServer' && notifierId == ntfQueueId' -> create
|
||||
| otherwise -> rotate
|
||||
(Nothing, Nothing) -> create
|
||||
_ -> rotate
|
||||
let newSubs = map (rqToNewSub ntfServer) rqs
|
||||
(cErrs, _) <- lift $ partitionErrs ntfSubConnId newSubs <$> withStoreBatch c (\db -> map (storeNewSub db) newSubs)
|
||||
notifyErrs c cErrs
|
||||
kickSMPWorkers rqs
|
||||
where
|
||||
create :: AM ()
|
||||
create = case action_ of
|
||||
-- action was set to NULL after worker internal error
|
||||
Nothing -> resetSubscription
|
||||
Just (action, _)
|
||||
-- subscription was marked for deletion / is being deleted
|
||||
| isDeleteNtfSubAction action -> do
|
||||
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
|
||||
then resetSubscription
|
||||
else withTokenServer $ \ntfServer -> do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NSANtf NSACreate)
|
||||
lift . void $ getNtfNTFWorker True c ntfServer
|
||||
| otherwise -> case action of
|
||||
NSANtf _ -> lift . void $ getNtfNTFWorker True c subNtfServer
|
||||
NSASMP _ -> lift . void $ getNtfSMPWorker True c smpServer
|
||||
rotate :: AM ()
|
||||
rotate = do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NSANtf NSARotate)
|
||||
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' (NSASMP NSASmpKey)
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
NSCDelete -> do
|
||||
sub_ <- withStore' c $ \db -> do
|
||||
supervisorUpdateNtfAction db connId (NSANtf NSADelete)
|
||||
getNtfSubscription db connId
|
||||
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
|
||||
case sub_ of
|
||||
(Just (NtfSubscription {ntfServer}, _)) -> lift . void $ getNtfNTFWorker True c ntfServer
|
||||
_ -> pure () -- err "NSCDelete - no subscription"
|
||||
rqToNewSub :: NtfServer -> RcvQueue -> NtfSubscription
|
||||
rqToNewSub ntfServer RcvQueue {userId, connId, server} = newNtfSubscription userId connId server Nothing ntfServer NASNew
|
||||
storeNewSub :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType ())
|
||||
storeNewSub db sub = first storeError <$> createNtfSubscription db sub (NSASMP NSASmpKey)
|
||||
resetSubs :: [(RcvQueue, NtfSubscription)] -> AM ()
|
||||
resetSubs rqSubs = do
|
||||
withTokenServer $ \ntfServer -> do
|
||||
let subsToReset = map (toResetSub ntfServer) rqSubs
|
||||
(cErrs, _) <- lift $ partitionErrs ntfSubConnId subsToReset <$> withStoreBatch' c (\db -> map (storeResetSub db) subsToReset)
|
||||
notifyErrs c cErrs
|
||||
let rqs = map fst rqSubs
|
||||
kickSMPWorkers rqs
|
||||
where
|
||||
toResetSub :: NtfServer -> (RcvQueue, NtfSubscription) -> NtfSubscription
|
||||
toResetSub ntfServer (rq, sub) =
|
||||
let RcvQueue {server = smpServer} = rq
|
||||
in sub {smpServer, ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
|
||||
storeResetSub :: DB.Connection -> NtfSubscription -> IO ()
|
||||
storeResetSub db sub = supervisorUpdateNtfSub db sub (NSASMP NSASmpKey)
|
||||
partitionQueueSubActions ::
|
||||
[(RcvQueue, Maybe NtfSupervisorSub)] ->
|
||||
( [RcvQueue], -- new subs
|
||||
[(RcvQueue, NtfSubscription)], -- reset subs
|
||||
[SMPServer], -- continue work (SMP)
|
||||
[NtfServer] -- continue work (Ntf)
|
||||
)
|
||||
partitionQueueSubActions = foldr decideSubWork ([], [], [], [])
|
||||
where
|
||||
-- sub = Nothing, needs to be created
|
||||
decideSubWork (rq, Nothing) (ns, rs, css, cns) = (rq : ns, rs, css, cns)
|
||||
decideSubWork (rq, Just (sub, subAction_)) (ns, rs, css, cns) =
|
||||
case (clientNtfCreds rq, ntfQueueId sub) of
|
||||
-- notifier ID created on SMP server (on ntf server subscription can be registered or not yet),
|
||||
-- need to clarify action
|
||||
(Just ClientNtfCreds {notifierId}, Just ntfQueueId')
|
||||
| sameSrvAddr (qServer rq) subSMPServer && notifierId == ntfQueueId' -> contOrReset
|
||||
| otherwise -> reset
|
||||
(Nothing, Nothing) -> contOrReset
|
||||
_ -> reset
|
||||
where
|
||||
NtfSubscription {ntfServer = subNtfServer, smpServer = subSMPServer} = sub
|
||||
contOrReset = case subAction_ of
|
||||
-- action was set to NULL after worker internal error
|
||||
Nothing -> reset
|
||||
Just (action, _)
|
||||
-- subscription was marked for deletion / is being deleted
|
||||
| isDeleteNtfSubAction action -> reset
|
||||
-- continue work on subscription (e.g. supervisor was repeatedly tasked with creating a subscription)
|
||||
| otherwise -> case action of
|
||||
NSASMP _ -> (ns, rs, qServer rq : css, cns)
|
||||
NSANtf _ -> (ns, rs, css, subNtfServer : cns)
|
||||
reset = (ns, (rq, sub) : rs, css, cns)
|
||||
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 (NSASMP NSASmpDelete)
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
|
||||
NSCNtfWorker ntfServer -> lift . void $ getNtfNTFWorker True c ntfServer
|
||||
NSCNtfSMPWorker smpServer -> lift . void $ getNtfSMPWorker True c smpServer
|
||||
(cErrs, rqs) <- lift $ partitionErrs id connIds' <$> withStoreBatch c (\db -> map (getQueue db) connIds')
|
||||
logInfo $ "processNtfCmd, NSCSmpDelete - length rqs = " <> tshow (length rqs)
|
||||
(cErrs', _) <- lift $ partitionErrs qConnId rqs <$> withStoreBatch' c (\db -> map (updateAction db) rqs)
|
||||
notifyErrs c (cErrs <> cErrs')
|
||||
kickSMPWorkers rqs
|
||||
where
|
||||
getQueue :: DB.Connection -> ConnId -> IO (Either AgentErrorType RcvQueue)
|
||||
getQueue db connId = first storeError <$> getPrimaryRcvQueue db connId
|
||||
updateAction :: DB.Connection -> RcvQueue -> IO ()
|
||||
updateAction db rq = supervisorUpdateNtfAction db (qConnId rq) (NSASMP NSASmpDelete)
|
||||
NSCDeleteSub -> void $ lift $ withStoreBatch' c $ \db -> map (deleteNtfSubscription' db) connIds'
|
||||
where
|
||||
kickSMPWorkers :: [RcvQueue] -> AM ()
|
||||
kickSMPWorkers rqs = do
|
||||
let smpServers = S.fromList $ map qServer rqs
|
||||
lift $ mapM_ (getNtfSMPWorker True c) smpServers
|
||||
|
||||
getNtfNTFWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
|
||||
getNtfNTFWorker hasWork c server = do
|
||||
@@ -143,6 +198,11 @@ getNtfSMPWorker hasWork c server = do
|
||||
ws <- asks $ ntfSMPWorkers . ntfSupervisor
|
||||
getAgentWorker "ntf_smp" hasWork c server ws $ runNtfSMPWorker c server
|
||||
|
||||
getNtfTknDelWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
|
||||
getNtfTknDelWorker hasWork c server = do
|
||||
ws <- asks $ ntfTknDelWorkers . ntfSupervisor
|
||||
getAgentWorker "ntf_tkn_del" hasWork c server ws $ runNtfTknDelWorker c server
|
||||
|
||||
withTokenServer :: (NtfServer -> AM ()) -> AM ()
|
||||
withTokenServer action = lift getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
|
||||
|
||||
@@ -153,153 +213,288 @@ runNtfWorker c srv Worker {doWork} =
|
||||
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
|
||||
where
|
||||
runNtfOperation :: AM ()
|
||||
runNtfOperation =
|
||||
withWork c doWork (`getNextNtfSubNTFAction` srv) $
|
||||
\nextSub@(NtfSubscription {connId}, _, _) -> do
|
||||
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
|
||||
processSub (sub@NtfSubscription {userId, connId, smpServer, ntfSubId}, action, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (lift $ rescheduleAction doWork ts actionTs) $
|
||||
case action of
|
||||
NSACreate ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer, ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
RcvQueue {clientNtfCreds} <- withStore c (`getPrimaryRcvQueue` connId)
|
||||
case clientNtfCreds of
|
||||
Just ClientNtfCreds {ntfPrivateKey, notifierId} -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfCreateAttempts
|
||||
nSubId <- agentNtfCreateSubscription c tknId tkn (SMPQueueNtf smpServer notifierId) ntfPrivateKey
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfCreated
|
||||
-- possible improvement: smaller retry until Active, less frequently (daily?) once Active
|
||||
let actionTs' = addUTCTime 30 ts
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NSANtf NSACheck) actionTs'
|
||||
_ -> workerInternalError c connId "NSACreate - no notifier queue credentials"
|
||||
_ -> workerInternalError c connId "NSACreate - no active token"
|
||||
NSACheck ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer} ->
|
||||
case ntfSubId of
|
||||
Just nSubId -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfCheckAttempts
|
||||
agentNtfCheckSubscription c nSubId tkn >>= \case
|
||||
NSAuth -> do
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NSASMP NSASmpKey) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
status -> updateSubNextCheck ts status
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfChecked
|
||||
Nothing -> workerInternalError c connId "NSACheck - no subscription ID"
|
||||
_ -> workerInternalError c connId "NSACheck - no active token"
|
||||
NSADelete ->
|
||||
deleteNtfSub $ do
|
||||
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
|
||||
withStore' c $ \db -> updateNtfSubscription db sub' (NSASMP NSASmpDelete) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
NSARotate ->
|
||||
deleteNtfSub $ do
|
||||
withStore' c $ \db -> deleteNtfSubscription db connId
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCCreate)
|
||||
runNtfOperation = do
|
||||
ntfBatchSize <- asks $ ntfBatchSize . config
|
||||
withWorkItems c doWork (\db -> getNextNtfSubNTFActions db srv ntfBatchSize) $ \nextSubs -> do
|
||||
logInfo $ "runNtfWorker - length nextSubs = " <> tshow (length nextSubs)
|
||||
currTs <- liftIO getCurrentTime
|
||||
let (creates, checks, deletes, rotates) = splitActions currTs nextSubs
|
||||
if null creates && null checks && null deletes && null rotates
|
||||
then
|
||||
let (_, _, firstActionTs) = L.head nextSubs
|
||||
in lift $ rescheduleWork doWork currTs firstActionTs
|
||||
else do
|
||||
retrySubActions c creates createSubs
|
||||
retrySubActions c checks checkSubs
|
||||
retrySubActions c deletes deleteSubs
|
||||
retrySubActions c rotates rotateSubs
|
||||
splitActions :: UTCTime -> NonEmpty (NtfSubNTFAction, NtfSubscription, NtfActionTs) -> ([NtfSubscription], [NtfSubscription], [NtfSubscription], [NtfSubscription])
|
||||
splitActions currTs = foldr addAction ([], [], [], [])
|
||||
where
|
||||
deleteNtfSub continue = case ntfSubId of
|
||||
Just nSubId ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer} -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfDelAttempts
|
||||
tryAgentError (agentNtfDeleteSubscription c nSubId tkn) >>= \case
|
||||
Left e | temporaryOrHostError e -> throwE e
|
||||
_ -> continue
|
||||
addAction (cmd, sub, ts) acc@(creates, checks, deletes, rotates) = case cmd of
|
||||
NSACreate -> (sub : creates, checks, deletes, rotates)
|
||||
NSACheck
|
||||
| ts <= currTs -> (creates, sub : checks, deletes, rotates)
|
||||
| otherwise -> acc
|
||||
NSADelete -> (creates, checks, sub : deletes, rotates)
|
||||
NSARotate -> (creates, checks, deletes, sub : rotates)
|
||||
createSubs :: [NtfSubscription] -> AM' [NtfSubscription]
|
||||
createSubs ntfSubs =
|
||||
getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer, ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
subsRqs_ <- zip ntfSubs <$> withStoreBatch c (\db -> map (getQueue db) ntfSubs)
|
||||
let (errs1, subs_, newSubs_) = splitSubs tknId subsRqs_
|
||||
incStatByUserId ntfServer ntfCreateAttempts subs_
|
||||
case (L.nonEmpty subs_, L.nonEmpty newSubs_) of
|
||||
(Just subs, Just newSubs) -> do
|
||||
rs <- L.zip subs <$> agentNtfCreateSubscriptions c tkn newSubs
|
||||
let (ntfSubs', errs2, nSubIds) = splitResults $ L.toList rs
|
||||
subs' = map fst nSubIds
|
||||
errs2' = map (first ntfSubConnId) errs2
|
||||
incStatByUserId ntfServer ntfCreated subs'
|
||||
ts <- liftIO getCurrentTime
|
||||
int <- asks $ ntfSubFirstCheckInterval . config
|
||||
let checkTs = addUTCTime int ts
|
||||
(errs3, _) <- partitionErrs ntfSubConnId subs' <$> withStoreBatch' c (\db -> map (updateSubNSACheck db checkTs) nSubIds)
|
||||
workerErrors c $ errs1 <> errs2' <> errs3
|
||||
pure ntfSubs'
|
||||
_ -> workerErrors c errs1 $> []
|
||||
_ -> do
|
||||
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSACreate - no active token")) ntfSubs
|
||||
workerErrors c errs
|
||||
pure []
|
||||
where
|
||||
getQueue :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType RcvQueue)
|
||||
getQueue db NtfSubscription {connId} = first storeError <$> getPrimaryRcvQueue db connId
|
||||
splitSubs :: NtfTokenId -> [(NtfSubscription, Either AgentErrorType RcvQueue)] -> ([(ConnId, AgentErrorType)], [NtfSubscription], [NewNtfEntity 'Subscription])
|
||||
splitSubs tknId = foldr splitSub ([], [], [])
|
||||
where
|
||||
splitSub (sub, rq) (errs, subs, newSubs) = case rq of
|
||||
Right RcvQueue {clientNtfCreds = Just creds} -> (errs, sub : subs, toNewSub sub creds : newSubs)
|
||||
Right _ -> ((ntfSubConnId sub, INTERNAL "NSACreate - no notifier queue credentials") : errs, subs, newSubs)
|
||||
Left e -> ((ntfSubConnId sub, e) : errs, subs, newSubs)
|
||||
toNewSub NtfSubscription {smpServer} ClientNtfCreds {ntfPrivateKey, notifierId} =
|
||||
NewNtfSub tknId (SMPQueueNtf smpServer notifierId) ntfPrivateKey
|
||||
updateSubNSACheck :: DB.Connection -> UTCTime -> (NtfSubscription, NtfSubscriptionId) -> IO ()
|
||||
updateSubNSACheck db checkTs (sub, nSubId) = updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NSANtf NSACheck) checkTs
|
||||
checkSubs :: [NtfSubscription] -> AM' [NtfSubscription]
|
||||
checkSubs ntfSubs =
|
||||
getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
let (errs1, subs_, subIds_) = splitSubs ntfSubs
|
||||
incStatByUserId ntfServer ntfCheckAttempts subs_
|
||||
case (L.nonEmpty subs_, L.nonEmpty subIds_) of
|
||||
(Just subs, Just subIds) -> do
|
||||
rs <- L.zip subs <$> agentNtfCheckSubscriptions c tkn subIds
|
||||
let (ntfSubs', errs2, nSubStatuses) = splitResults $ L.toList rs
|
||||
subs' = map fst nSubStatuses
|
||||
(errs2', authSubs) = partitionEithers $ map (\case (sub, NTF _ SMP.AUTH) -> Right sub; e -> Left $ first ntfSubConnId e) errs2
|
||||
incStatByUserId ntfServer ntfChecked subs'
|
||||
ts <- liftIO getCurrentTime
|
||||
int <- asks $ ntfSubCheckInterval . config
|
||||
let nextCheckTs = addUTCTime int ts
|
||||
(errs3, srvs) <- partitionErrs ntfSubConnId subs' <$> withStoreBatch' c (\db -> map (updateSub db ntfServer ts nextCheckTs) nSubStatuses)
|
||||
(errs4, srvs') <- partitionErrs ntfSubConnId authSubs <$> withStoreBatch' c (\db -> map (recreateNtfSub db ntfServer ts) authSubs)
|
||||
mapM_ (getNtfSMPWorker True c) $ S.fromList (catMaybes srvs <> srvs')
|
||||
workerErrors c $ errs1 <> errs2' <> errs3 <> errs4
|
||||
pure ntfSubs'
|
||||
_ -> workerErrors c errs1 $> []
|
||||
_ -> do
|
||||
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSACheck - no active token")) ntfSubs
|
||||
workerErrors c errs
|
||||
pure []
|
||||
where
|
||||
splitSubs :: [NtfSubscription] -> ([(ConnId, AgentErrorType)], [NtfSubscription], [NtfSubscriptionId])
|
||||
splitSubs = foldr splitSub ([], [], [])
|
||||
where
|
||||
splitSub sub (errs, subs, subIds) = case sub of
|
||||
NtfSubscription {ntfSubId = Just subId} -> (errs, sub : subs, subId : subIds)
|
||||
_ -> ((ntfSubConnId sub, INTERNAL "NSACheck - no subscription ID") : errs, subs, subIds)
|
||||
updateSub :: DB.Connection -> NtfServer -> UTCTime -> UTCTime -> (NtfSubscription, NtfSubStatus) -> IO (Maybe SMPServer)
|
||||
updateSub db ntfServer ts nextCheckTs (sub, status)
|
||||
| ntfShouldSubscribe status =
|
||||
let sub' = sub {ntfSubStatus = NASCreated status}
|
||||
in Nothing <$ updateNtfSubscription db sub' (NSANtf NSACheck) nextCheckTs
|
||||
-- ntf server stopped subscribing to this queue
|
||||
| otherwise = Just <$> recreateNtfSub db ntfServer ts sub
|
||||
recreateNtfSub :: DB.Connection -> NtfServer -> UTCTime -> NtfSubscription -> IO SMPServer
|
||||
recreateNtfSub db ntfServer ts sub@NtfSubscription {smpServer} =
|
||||
let sub' = sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew}
|
||||
in smpServer <$ updateNtfSubscription db sub' (NSASMP NSASmpKey) ts
|
||||
incStatByUserId :: NtfServer -> (AgentNtfServerStats -> TVar Int) -> [NtfSubscription] -> AM' ()
|
||||
incStatByUserId ntfServer sel ss =
|
||||
forM_ (M.assocs userIdsCounts) $ \(userId, count) ->
|
||||
atomically $ incNtfServerStat' c userId ntfServer sel count
|
||||
where
|
||||
userIdsCounts = foldl' (\acc NtfSubscription {userId} -> M.insertWith (+) userId 1 acc) M.empty ss
|
||||
-- NSADelete and NSARotate are deprecated, but their processing is kept for legacy db records;
|
||||
-- These actions are not batched
|
||||
deleteSubs :: [NtfSubscription] -> AM' [NtfSubscription]
|
||||
deleteSubs ntfSubs = do
|
||||
retrySubs_ <- mapM (runCatching deleteSub) ntfSubs
|
||||
pure $ catMaybes retrySubs_
|
||||
where
|
||||
deleteSub :: NtfSubscription -> AM (Maybe NtfSubscription)
|
||||
deleteSub sub@NtfSubscription {smpServer} =
|
||||
deleteNtfSub sub $ do
|
||||
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' c $ \db -> updateNtfSubscription db sub' (NSASMP NSASmpDelete) ts
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
rotateSubs :: [NtfSubscription] -> AM' [NtfSubscription]
|
||||
rotateSubs ntfSubs = do
|
||||
retrySubs_ <- mapM (runCatching rotateSub) ntfSubs
|
||||
pure $ catMaybes retrySubs_
|
||||
where
|
||||
rotateSub :: NtfSubscription -> AM (Maybe NtfSubscription)
|
||||
rotateSub sub@NtfSubscription {connId} =
|
||||
deleteNtfSub sub $ do
|
||||
withStore' c $ \db -> deleteNtfSubscription db connId
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (NSCCreate, [connId])
|
||||
runCatching :: (NtfSubscription -> AM (Maybe NtfSubscription)) -> NtfSubscription -> AM' (Maybe NtfSubscription)
|
||||
runCatching action sub@NtfSubscription {connId} =
|
||||
fromRight Nothing
|
||||
<$> runExceptT (action sub `catchAgentError` \e -> workerInternalError c connId (show e) $> Nothing)
|
||||
-- deleteNtfSub is only used in NSADelete and NSARotate, so also deprecated
|
||||
deleteNtfSub :: NtfSubscription -> AM () -> AM (Maybe NtfSubscription)
|
||||
deleteNtfSub sub@NtfSubscription {userId, ntfSubId} continue = case ntfSubId of
|
||||
Just nSubId ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer} -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfDelAttempts
|
||||
tryAgentError (agentNtfDeleteSubscription c nSubId tkn) >>= \case
|
||||
Right _ -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfDeleted
|
||||
Nothing -> continue
|
||||
_ -> continue
|
||||
updateSubNextCheck ts toStatus = do
|
||||
checkInterval <- asks $ ntfSubCheckInterval . config
|
||||
let nextCheckTs = addUTCTime checkInterval ts
|
||||
updateSub (NASCreated toStatus) (NSANtf NSACheck) nextCheckTs
|
||||
updateSub toStatus toAction actionTs' =
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
|
||||
continue'
|
||||
Left e
|
||||
| temporaryOrHostError e -> pure $ Just sub -- don't continue, retry
|
||||
| otherwise -> continue'
|
||||
Nothing -> continue'
|
||||
_ -> continue'
|
||||
where
|
||||
continue' = continue $> Nothing -- continue without retry
|
||||
|
||||
runNtfSMPWorker :: AgentClient -> SMPServer -> Worker -> AM ()
|
||||
runNtfSMPWorker c srv Worker {doWork} = do
|
||||
env <- ask
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
ExceptT . liftIO . agentOperationBracket c AONtfNetwork throwWhenInactive $
|
||||
runReaderT (runExceptT runNtfSMPOperation) env
|
||||
runNtfSMPWorker c srv Worker {doWork} = forever $ do
|
||||
waitForWork doWork
|
||||
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfSMPOperation
|
||||
where
|
||||
runNtfSMPOperation =
|
||||
withWork c doWork (`getNextNtfSubSMPAction` srv) $
|
||||
\nextSub@(NtfSubscription {connId}, _, _) -> do
|
||||
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM ()
|
||||
processSub (sub@NtfSubscription {connId, ntfServer}, smpAction, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (lift $ rescheduleAction doWork ts actionTs) $
|
||||
case smpAction of
|
||||
NSASmpKey ->
|
||||
lift getNtfToken >>= \case
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
rq <- withStore c (`getPrimaryRcvQueue` connId)
|
||||
C.AuthAlg a <- asks (rcvAuthAlg . config)
|
||||
g <- asks random
|
||||
(ntfPublicKey, ntfPrivateKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
(rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g
|
||||
(notifierId, rcvNtfSrvPubDhKey) <- enableQueueNotifications c rq ntfPublicKey rcvNtfPubDhKey
|
||||
let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
|
||||
withStore' c $ \db -> do
|
||||
setRcvQueueNtfCreds db connId $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
|
||||
updateNtfSubscription db sub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NSANtf NSACreate) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCNtfWorker ntfServer)
|
||||
_ -> workerInternalError c connId "NSASmpKey - no active token"
|
||||
NSASmpDelete -> do
|
||||
-- TODO should we remove it after successful removal from the server?
|
||||
rq_ <- withStore' c $ \db -> do
|
||||
setRcvQueueNtfCreds db connId Nothing
|
||||
getPrimaryRcvQueue db connId
|
||||
mapM_ (disableQueueNotifications c) rq_
|
||||
withStore' c $ \db -> deleteNtfSubscription db connId
|
||||
runNtfSMPOperation :: AM ()
|
||||
runNtfSMPOperation = do
|
||||
ntfBatchSize <- asks $ ntfBatchSize . config
|
||||
withWorkItems c doWork (\db -> getNextNtfSubSMPActions db srv ntfBatchSize) $ \nextSubs -> do
|
||||
logInfo $ "runNtfSMPWorker - length nextSubs = " <> tshow (length nextSubs)
|
||||
let (creates, deletes) = splitActions nextSubs
|
||||
retrySubActions c creates createNotifierKeys
|
||||
retrySubActions c deletes deleteNotifierKeys
|
||||
splitActions :: NonEmpty (NtfSubSMPAction, NtfSubscription) -> ([NtfSubscription], [NtfSubscription])
|
||||
splitActions = foldr addAction ([], [])
|
||||
where
|
||||
addAction (cmd, sub) (creates, deletes) = case cmd of
|
||||
NSASmpKey -> (sub : creates, deletes)
|
||||
NSASmpDelete -> (creates, sub : deletes)
|
||||
createNotifierKeys :: [NtfSubscription] -> AM' [NtfSubscription]
|
||||
createNotifierKeys ntfSubs =
|
||||
getNtfToken >>= \case
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
(errs1, subRqKeys) <- prepareQueueSmpKey ntfSubs
|
||||
rs <- enableQueuesNtfs c subRqKeys
|
||||
let (subRqKeys', errs2, successes) = splitResults rs
|
||||
ntfSubs' = map eqnrNtfSub subRqKeys'
|
||||
errs2' = map (first (qConnId . eqnrRq)) errs2
|
||||
ts <- liftIO getCurrentTime
|
||||
(errs3, srvs) <- partitionErrs (qConnId . eqnrRq . fst) successes <$> withStoreBatch' c (\db -> map (storeNtfSubCreds db ts) successes)
|
||||
mapM_ (getNtfNTFWorker True c) $ S.fromList srvs
|
||||
workerErrors c $ errs1 <> errs2' <> errs3
|
||||
pure ntfSubs'
|
||||
_ -> do
|
||||
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSASmpKey - no active token")) ntfSubs
|
||||
workerErrors c errs
|
||||
pure []
|
||||
where
|
||||
prepareQueueSmpKey :: [NtfSubscription] -> AM' ([(ConnId, AgentErrorType)], [EnableQueueNtfReq])
|
||||
prepareQueueSmpKey subs = do
|
||||
alg <- asks (rcvAuthAlg . config)
|
||||
g <- asks random
|
||||
partitionErrs ntfSubConnId subs <$> withStoreBatch c (\db -> map (getQueue db alg g) subs)
|
||||
where
|
||||
getQueue :: DB.Connection -> C.AuthAlg -> TVar ChaChaDRG -> NtfSubscription -> IO (Either AgentErrorType EnableQueueNtfReq)
|
||||
getQueue db (C.AuthAlg a) g sub = fmap (first storeError) $ runExceptT $ do
|
||||
rq <- ExceptT $ getPrimaryRcvQueue db (ntfSubConnId sub)
|
||||
authKeyPair <- atomically $ C.generateAuthKeyPair a g
|
||||
rcvNtfKeyPair <- atomically $ C.generateKeyPair g
|
||||
pure (EnableQueueNtfReq sub rq authKeyPair rcvNtfKeyPair)
|
||||
storeNtfSubCreds :: DB.Connection -> UTCTime -> (EnableQueueNtfReq, (SMP.NotifierId, SMP.RcvNtfPublicDhKey)) -> IO NtfServer
|
||||
storeNtfSubCreds db ts (EnableQueueNtfReq {eqnrNtfSub, eqnrAuthKeyPair = (ntfPublicKey, ntfPrivateKey), eqnrRcvKeyPair = (_, pk)}, (notifierId, srvPubDhKey)) = do
|
||||
let NtfSubscription {ntfServer} = eqnrNtfSub
|
||||
rcvNtfDhSecret = C.dh' srvPubDhKey pk
|
||||
setRcvQueueNtfCreds db (ntfSubConnId eqnrNtfSub) $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
|
||||
updateNtfSubscription db eqnrNtfSub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NSANtf NSACreate) ts
|
||||
pure ntfServer
|
||||
deleteNotifierKeys :: [NtfSubscription] -> AM' [NtfSubscription]
|
||||
deleteNotifierKeys ntfSubs = do
|
||||
(errs1, subRqs) <- partitionErrs ntfSubConnId ntfSubs <$> withStoreBatch c (\db -> map (resetCredsGetQueue db) ntfSubs)
|
||||
rs <- disableQueuesNtfs c subRqs
|
||||
let (subRqs', errs2, successes) = splitResults rs
|
||||
ntfSubs' = map fst subRqs'
|
||||
errs2' = map (first (qConnId . snd)) errs2
|
||||
disabledRqs = map (snd . fst) successes
|
||||
(errs3, _) <- partitionErrs qConnId disabledRqs <$> withStoreBatch' c (\db -> map (deleteSub db) disabledRqs)
|
||||
workerErrors c $ errs1 <> errs2' <> errs3
|
||||
pure ntfSubs'
|
||||
where
|
||||
resetCredsGetQueue :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType DisableQueueNtfReq)
|
||||
resetCredsGetQueue db sub@NtfSubscription {connId} = fmap (first storeError) $ runExceptT $ do
|
||||
liftIO $ setRcvQueueNtfCreds db connId Nothing
|
||||
rq <- ExceptT $ getPrimaryRcvQueue db connId
|
||||
pure (sub, rq)
|
||||
deleteSub :: DB.Connection -> RcvQueue -> IO ()
|
||||
deleteSub db rq = deleteNtfSubscription db (qConnId rq)
|
||||
|
||||
rescheduleAction :: TMVar () -> UTCTime -> UTCTime -> AM' Bool
|
||||
rescheduleAction doWork ts actionTs
|
||||
| actionTs <= ts = pure False
|
||||
| otherwise = do
|
||||
void . atomically $ tryTakeTMVar doWork
|
||||
void . forkIO $ do
|
||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
|
||||
atomically $ hasWorkToDo' doWork
|
||||
pure True
|
||||
retrySubActions :: AgentClient -> [NtfSubscription] -> ([NtfSubscription] -> AM' [NtfSubscription]) -> AM ()
|
||||
retrySubActions _ [] _ = pure ()
|
||||
retrySubActions c subs action = do
|
||||
v <- newTVarIO subs
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
subs' <- readTVarIO v
|
||||
retrySubs <- lift $ action subs'
|
||||
unless (null retrySubs) $ do
|
||||
atomically $ writeTVar v retrySubs
|
||||
retryNetworkLoop c loop
|
||||
|
||||
retryOnError :: AgentClient -> Text -> AM () -> (AgentErrorType -> AM ()) -> AgentErrorType -> AM ()
|
||||
retryOnError c name loop done e = do
|
||||
logError $ name <> " error: " <> tshow e
|
||||
case e of
|
||||
BROKER _ NETWORK -> retryLoop
|
||||
BROKER _ TIMEOUT -> retryLoop
|
||||
_ -> done e
|
||||
-- (temporary errs, other errs, successes)
|
||||
splitResults :: [(a, Either AgentErrorType r)] -> ([a], [(a, AgentErrorType)], [(a, r)])
|
||||
splitResults = foldr addRes ([], [], [])
|
||||
where
|
||||
retryLoop = do
|
||||
atomically $ endAgentOperation c AONtfNetwork
|
||||
liftIO $ throwWhenInactive c
|
||||
atomically $ beginAgentOperation c AONtfNetwork
|
||||
loop
|
||||
addRes (a, r_) (as, errs, rs) = case r_ of
|
||||
Right r -> (as, errs, (a, r) : rs)
|
||||
Left e
|
||||
| temporaryOrHostError e -> (a : as, errs, rs)
|
||||
| otherwise -> (as, (a, e) : errs, rs)
|
||||
|
||||
rescheduleWork :: TMVar () -> UTCTime -> UTCTime -> AM' ()
|
||||
rescheduleWork doWork ts actionTs = do
|
||||
void . atomically $ tryTakeTMVar doWork
|
||||
void . forkIO $ do
|
||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
|
||||
atomically $ hasWorkToDo' doWork
|
||||
|
||||
retryNetworkLoop :: AgentClient -> AM () -> AM ()
|
||||
retryNetworkLoop c loop = do
|
||||
atomically $ endAgentOperation c AONtfNetwork
|
||||
liftIO $ throwWhenInactive c
|
||||
atomically $ beginAgentOperation c AONtfNetwork
|
||||
loop
|
||||
|
||||
workerErrors :: AgentClient -> [(ConnId, AgentErrorType)] -> AM' ()
|
||||
workerErrors c connErrs =
|
||||
unless (null connErrs) $ do
|
||||
void $ withStoreBatch' c (\db -> map (setNullNtfSubscriptionAction db . fst) connErrs)
|
||||
notifyErrs c connErrs
|
||||
|
||||
workerInternalError :: AgentClient -> ConnId -> String -> AM ()
|
||||
workerInternalError c connId internalErrStr = do
|
||||
@@ -311,6 +506,14 @@ notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m ()
|
||||
notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, AEvt SAEConn $ ERR $ INTERNAL internalErrStr)
|
||||
{-# INLINE notifyInternalError #-}
|
||||
|
||||
notifyInternalError' :: MonadIO m => AgentClient -> String -> m ()
|
||||
notifyInternalError' AgentClient {subQ} internalErrStr = atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL internalErrStr)
|
||||
{-# INLINE notifyInternalError' #-}
|
||||
|
||||
notifyErrs :: MonadIO m => AgentClient -> [(ConnId, AgentErrorType)] -> m ()
|
||||
notifyErrs AgentClient {subQ} connErrs = unless (null connErrs) $ atomically $ writeTBQueue subQ ("", "", AEvt SAENone $ ERRS connErrs)
|
||||
{-# INLINE notifyErrs #-}
|
||||
|
||||
getNtfToken :: AM' (Maybe NtfToken)
|
||||
getNtfToken = do
|
||||
tkn <- asks $ ntfTkn . ntfSupervisor
|
||||
@@ -322,7 +525,7 @@ nsUpdateToken ns tkn = writeTVar (ntfTkn ns) $ Just tkn
|
||||
nsRemoveNtfToken :: NtfSupervisor -> STM ()
|
||||
nsRemoveNtfToken ns = writeTVar (ntfTkn ns) Nothing
|
||||
|
||||
sendNtfSubCommand :: NtfSupervisor -> (ConnId, NtfSupervisorCommand) -> STM ()
|
||||
sendNtfSubCommand :: NtfSupervisor -> (NtfSupervisorCommand, NonEmpty ConnId) -> STM ()
|
||||
sendNtfSubCommand ns cmd = do
|
||||
tkn <- readTVar (ntfTkn ns)
|
||||
when (instantNotifications tkn) $ writeTBQueue (ntfSubQ ns) cmd
|
||||
@@ -332,10 +535,51 @@ instantNotifications = \case
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> True
|
||||
_ -> False
|
||||
|
||||
deleteToken :: AgentClient -> NtfToken -> AM ()
|
||||
deleteToken c tkn@NtfToken {ntfServer, ntfTokenId, ntfPrivKey} = do
|
||||
setToDelete <- withStore' c $ \db -> do
|
||||
removeNtfToken db tkn
|
||||
case ntfTokenId of
|
||||
Just tknId -> addNtfTokenToDelete db ntfServer ntfPrivKey tknId $> True
|
||||
Nothing -> pure False
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ nsRemoveNtfToken ns
|
||||
when setToDelete $ void $ lift $ getNtfTknDelWorker True c ntfServer
|
||||
|
||||
runNtfTknDelWorker :: AgentClient -> NtfServer -> Worker -> AM ()
|
||||
runNtfTknDelWorker c srv Worker {doWork} =
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
|
||||
where
|
||||
runNtfOperation :: AM ()
|
||||
runNtfOperation =
|
||||
withWork c doWork (`getNextNtfTokenToDelete` srv) $
|
||||
\nextTknToDelete -> do
|
||||
logInfo $ "runNtfTknDelWorker, nextTknToDelete " <> tshow nextTknToDelete
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processTknToDelete nextTknToDelete `catchAgentError` retryTmpError loop nextTknToDelete
|
||||
retryTmpError :: AM () -> NtfTokenToDelete -> AgentErrorType -> AM ()
|
||||
retryTmpError loop (tknDbId, _, _) e = do
|
||||
logError $ "ntf tkn del error: " <> tshow e
|
||||
if temporaryOrHostError e
|
||||
then retryNetworkLoop c loop
|
||||
else do
|
||||
withStore' c $ \db -> deleteNtfTokenToDelete db tknDbId
|
||||
notifyInternalError' c (show e)
|
||||
processTknToDelete :: NtfTokenToDelete -> AM ()
|
||||
processTknToDelete (tknDbId, ntfPrivKey, tknId) = do
|
||||
agentNtfDeleteToken c srv ntfPrivKey tknId
|
||||
withStore' c $ \db -> deleteNtfTokenToDelete db tknDbId
|
||||
|
||||
closeNtfSupervisor :: NtfSupervisor -> IO ()
|
||||
closeNtfSupervisor ns = do
|
||||
stopWorkers $ ntfWorkers ns
|
||||
stopWorkers $ ntfSMPWorkers ns
|
||||
stopWorkers $ ntfTknDelWorkers ns
|
||||
where
|
||||
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
|
||||
|
||||
|
||||
@@ -277,14 +277,14 @@ supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPA
|
||||
e2eEncConnInfoLength :: VersionSMPA -> PQSupport -> Int
|
||||
e2eEncConnInfoLength v = \case
|
||||
-- reduced by 3726 (roughly the increase of message ratchet header size + key and ciphertext in reply link)
|
||||
PQSupportOn | v >= pqdrSMPAgentVersion -> 11122
|
||||
_ -> 14848
|
||||
PQSupportOn | v >= pqdrSMPAgentVersion -> 11106
|
||||
_ -> 14832
|
||||
|
||||
e2eEncAgentMsgLength :: VersionSMPA -> PQSupport -> Int
|
||||
e2eEncAgentMsgLength v = \case
|
||||
-- reduced by 2222 (the increase of message ratchet header size)
|
||||
PQSupportOn | v >= pqdrSMPAgentVersion -> 13634
|
||||
_ -> 15856
|
||||
PQSupportOn | v >= pqdrSMPAgentVersion -> 13618
|
||||
_ -> 15840
|
||||
|
||||
-- | SMP agent event
|
||||
type ATransmission = (ACorrId, AEntityId, AEvt)
|
||||
@@ -344,6 +344,7 @@ data AEvent (e :: AEntity) where
|
||||
INFO :: PQSupport -> ConnInfo -> AEvent AEConn
|
||||
CON :: PQEncryption -> AEvent AEConn -- notification that connection is established
|
||||
END :: AEvent AEConn
|
||||
DELD :: AEvent AEConn
|
||||
CONNECT :: AProtocolType -> TransportHost -> AEvent AENone
|
||||
DISCONNECT :: AProtocolType -> TransportHost -> AEvent AENone
|
||||
DOWN :: SMPServer -> [ConnId] -> AEvent AENone
|
||||
@@ -365,6 +366,7 @@ data AEvent (e :: AEntity) where
|
||||
OK :: AEvent AEConn
|
||||
JOINED :: SndQueueSecured -> AEvent AEConn
|
||||
ERR :: AgentErrorType -> AEvent AEConn
|
||||
ERRS :: [(ConnId, AgentErrorType)] -> AEvent AENone
|
||||
SUSPENDED :: AEvent AENone
|
||||
RFPROG :: Int64 -> Int64 -> AEvent AERcvFile
|
||||
RFDONE :: FilePath -> AEvent AERcvFile
|
||||
@@ -413,6 +415,7 @@ data AEventTag (e :: AEntity) where
|
||||
INFO_ :: AEventTag AEConn
|
||||
CON_ :: AEventTag AEConn
|
||||
END_ :: AEventTag AEConn
|
||||
DELD_ :: AEventTag AEConn
|
||||
CONNECT_ :: AEventTag AENone
|
||||
DISCONNECT_ :: AEventTag AENone
|
||||
DOWN_ :: AEventTag AENone
|
||||
@@ -434,6 +437,7 @@ data AEventTag (e :: AEntity) where
|
||||
OK_ :: AEventTag AEConn
|
||||
JOINED_ :: AEventTag AEConn
|
||||
ERR_ :: AEventTag AEConn
|
||||
ERRS_ :: AEventTag AENone
|
||||
SUSPENDED_ :: AEventTag AENone
|
||||
-- XFTP commands and responses
|
||||
RFDONE_ :: AEventTag AERcvFile
|
||||
@@ -466,6 +470,7 @@ aEventTag = \case
|
||||
INFO {} -> INFO_
|
||||
CON _ -> CON_
|
||||
END -> END_
|
||||
DELD -> DELD_
|
||||
CONNECT {} -> CONNECT_
|
||||
DISCONNECT {} -> DISCONNECT_
|
||||
DOWN {} -> DOWN_
|
||||
@@ -487,6 +492,7 @@ aEventTag = \case
|
||||
OK -> OK_
|
||||
JOINED _ -> JOINED_
|
||||
ERR _ -> ERR_
|
||||
ERRS _ -> ERRS_
|
||||
SUSPENDED -> SUSPENDED_
|
||||
RFPROG {} -> RFPROG_
|
||||
RFDONE {} -> RFDONE_
|
||||
|
||||
@@ -150,7 +150,15 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
updateNtfMode,
|
||||
updateNtfToken,
|
||||
removeNtfToken,
|
||||
addNtfTokenToDelete,
|
||||
deleteExpiredNtfTokensToDelete,
|
||||
NtfTokenToDelete,
|
||||
getNextNtfTokenToDelete,
|
||||
markNtfTokenToDeleteFailed_, -- exported for tests
|
||||
getPendingDelTknServers,
|
||||
deleteNtfTokenToDelete,
|
||||
-- Notification subscription persistence
|
||||
NtfSupervisorSub,
|
||||
getNtfSubscription,
|
||||
createNtfSubscription,
|
||||
supervisorUpdateNtfSub,
|
||||
@@ -158,9 +166,10 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
updateNtfSubscription,
|
||||
setNullNtfSubscriptionAction,
|
||||
deleteNtfSubscription,
|
||||
getNextNtfSubNTFAction,
|
||||
deleteNtfSubscription',
|
||||
getNextNtfSubNTFActions,
|
||||
markNtfSubActionNtfFailed_, -- exported for tests
|
||||
getNextNtfSubSMPAction,
|
||||
getNextNtfSubSMPActions,
|
||||
markNtfSubActionSMPFailed_, -- exported for tests
|
||||
getActiveNtfToken,
|
||||
getNtfRcvQueue,
|
||||
@@ -1061,17 +1070,26 @@ getPendingQueueMsg db connId SndQueue {dbQueueId} =
|
||||
|
||||
getWorkItem :: Show i => ByteString -> IO (Maybe i) -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> IO (Either StoreError (Maybe a))
|
||||
getWorkItem itemName getId getItem markFailed =
|
||||
runExceptT $ handleErr "getId" getId >>= mapM tryGetItem
|
||||
runExceptT $ handleWrkErr itemName "getId" getId >>= mapM (tryGetItem itemName getItem markFailed)
|
||||
|
||||
getWorkItems :: Show i => ByteString -> IO [i] -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> IO (Either StoreError [Either StoreError a])
|
||||
getWorkItems itemName getIds getItem markFailed =
|
||||
runExceptT $ handleWrkErr itemName "getIds" getIds >>= mapM (tryE . tryGetItem itemName getItem markFailed)
|
||||
|
||||
tryGetItem :: Show i => ByteString -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> i -> ExceptT StoreError IO a
|
||||
tryGetItem itemName getItem markFailed itemId = ExceptT (getItem itemId) `catchStoreError` \e -> mark >> throwE e
|
||||
where
|
||||
tryGetItem itemId = ExceptT (getItem itemId) `catchStoreErrors` \e -> mark itemId >> throwE e
|
||||
mark itemId = handleErr ("markFailed ID " <> bshow itemId) $ markFailed itemId
|
||||
catchStoreErrors = catchAllErrors (SEInternal . bshow)
|
||||
-- Errors caught by this function will suspend worker as if there is no more work,
|
||||
handleErr :: ByteString -> IO a -> ExceptT StoreError IO a
|
||||
handleErr opName action = ExceptT $ first mkError <$> E.try action
|
||||
where
|
||||
mkError :: E.SomeException -> StoreError
|
||||
mkError e = SEWorkItemError $ itemName <> " " <> opName <> " error: " <> bshow e
|
||||
mark = handleWrkErr itemName ("markFailed ID " <> bshow itemId) $ markFailed itemId
|
||||
|
||||
catchStoreError :: ExceptT StoreError IO a -> (StoreError -> ExceptT StoreError IO a) -> ExceptT StoreError IO a
|
||||
catchStoreError = catchAllErrors (SEInternal . bshow)
|
||||
|
||||
-- Errors caught by this function will suspend worker as if there is no more work,
|
||||
handleWrkErr :: ByteString -> ByteString -> IO a -> ExceptT StoreError IO a
|
||||
handleWrkErr itemName opName action = ExceptT $ first mkError <$> E.try action
|
||||
where
|
||||
mkError :: E.SomeException -> StoreError
|
||||
mkError e = SEWorkItemError $ itemName <> " " <> opName <> " error: " <> bshow e
|
||||
|
||||
updatePendingMsgRIState :: DB.Connection -> ConnId -> InternalId -> RI2State -> IO ()
|
||||
updatePendingMsgRIState db connId msgId RI2State {slowInterval, fastInterval} =
|
||||
@@ -1475,7 +1493,73 @@ removeNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer
|
||||
|]
|
||||
(provider, token, host, port)
|
||||
|
||||
getNtfSubscription :: DB.Connection -> ConnId -> IO (Maybe (NtfSubscription, Maybe (NtfSubAction, NtfActionTs)))
|
||||
addNtfTokenToDelete :: DB.Connection -> NtfServer -> C.APrivateAuthKey -> NtfTokenId -> IO ()
|
||||
addNtfTokenToDelete db ProtocolServer {host, port, keyHash} ntfPrivKey tknId =
|
||||
DB.execute db "INSERT INTO ntf_tokens_to_delete (ntf_host, ntf_port, ntf_key_hash, tkn_id, tkn_priv_key) VALUES (?,?,?,?,?)" (host, port, keyHash, tknId, ntfPrivKey)
|
||||
|
||||
deleteExpiredNtfTokensToDelete :: DB.Connection -> NominalDiffTime -> IO ()
|
||||
deleteExpiredNtfTokensToDelete db ttl = do
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.execute db "DELETE FROM ntf_tokens_to_delete WHERE created_at < ?" (Only cutoffTs)
|
||||
|
||||
type NtfTokenToDelete = (Int64, C.APrivateAuthKey, NtfTokenId)
|
||||
|
||||
getNextNtfTokenToDelete :: DB.Connection -> NtfServer -> IO (Either StoreError (Maybe NtfTokenToDelete))
|
||||
getNextNtfTokenToDelete db (NtfServer ntfHost ntfPort _) =
|
||||
getWorkItem "ntf tkn del" getNtfTknDbId getNtfTknToDelete (markNtfTokenToDeleteFailed_ db)
|
||||
where
|
||||
getNtfTknDbId :: IO (Maybe Int64)
|
||||
getNtfTknDbId =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT ntf_token_to_delete_id
|
||||
FROM ntf_tokens_to_delete
|
||||
WHERE ntf_host = ? AND ntf_port = ?
|
||||
AND del_failed = 0
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
|]
|
||||
(ntfHost, ntfPort)
|
||||
getNtfTknToDelete :: Int64 -> IO (Either StoreError NtfTokenToDelete)
|
||||
getNtfTknToDelete tknDbId =
|
||||
firstRow ntfTokenToDelete err $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT tkn_priv_key, tkn_id
|
||||
FROM ntf_tokens_to_delete
|
||||
WHERE ntf_token_to_delete_id = ?
|
||||
|]
|
||||
(Only tknDbId)
|
||||
where
|
||||
err = SEInternal $ "ntf token to delete " <> bshow tknDbId <> " returned []"
|
||||
ntfTokenToDelete (tknPrivKey, tknId) = (tknDbId, tknPrivKey, tknId)
|
||||
|
||||
markNtfTokenToDeleteFailed_ :: DB.Connection -> Int64 -> IO ()
|
||||
markNtfTokenToDeleteFailed_ db tknDbId =
|
||||
DB.execute db "UPDATE ntf_tokens_to_delete SET del_failed = 1 where ntf_token_to_delete_id = ?" (Only tknDbId)
|
||||
|
||||
getPendingDelTknServers :: DB.Connection -> IO [NtfServer]
|
||||
getPendingDelTknServers db =
|
||||
map toNtfServer
|
||||
<$> DB.query_
|
||||
db
|
||||
[sql|
|
||||
SELECT DISTINCT ntf_host, ntf_port, ntf_key_hash
|
||||
FROM ntf_tokens_to_delete
|
||||
|]
|
||||
where
|
||||
toNtfServer (host, port, keyHash) = NtfServer host port keyHash
|
||||
|
||||
deleteNtfTokenToDelete :: DB.Connection -> Int64 -> IO ()
|
||||
deleteNtfTokenToDelete db tknDbId =
|
||||
DB.execute db "DELETE FROM ntf_tokens_to_delete WHERE ntf_token_to_delete_id = ?" (Only tknDbId)
|
||||
|
||||
type NtfSupervisorSub = (NtfSubscription, Maybe (NtfSubAction, NtfActionTs))
|
||||
|
||||
getNtfSubscription :: DB.Connection -> ConnId -> IO (Maybe NtfSupervisorSub)
|
||||
getNtfSubscription db connId =
|
||||
maybeFirstRow ntfSubscription $
|
||||
DB.query
|
||||
@@ -1491,7 +1575,7 @@ getNtfSubscription db connId =
|
||||
|]
|
||||
(Only connId)
|
||||
where
|
||||
ntfSubscription ((userId, smpHost, smpPort, smpKeyHash, ntfHost, ntfPort, ntfKeyHash ) :. (ntfQueueId, ntfSubId, ntfSubStatus, ntfAction_, smpAction_, actionTs_)) =
|
||||
ntfSubscription ((userId, smpHost, smpPort, smpKeyHash, ntfHost, ntfPort, ntfKeyHash) :. (ntfQueueId, ntfSubId, ntfSubStatus, ntfAction_, smpAction_, actionTs_)) =
|
||||
let smpServer = SMPServer smpHost smpPort smpKeyHash
|
||||
ntfServer = NtfServer ntfHost ntfPort ntfKeyHash
|
||||
action = case (ntfAction_, smpAction_, actionTs_) of
|
||||
@@ -1521,16 +1605,19 @@ createNtfSubscription db ntfSubscription action = runExceptT $ do
|
||||
(ntfSubAction, ntfSubSMPAction) = ntfSubAndSMPAction action
|
||||
|
||||
supervisorUpdateNtfSub :: DB.Connection -> NtfSubscription -> NtfSubAction -> IO ()
|
||||
supervisorUpdateNtfSub db NtfSubscription {connId, ntfQueueId, ntfServer = (NtfServer ntfHost ntfPort _), ntfSubId, ntfSubStatus} action = do
|
||||
supervisorUpdateNtfSub db NtfSubscription {connId, smpServer = (SMPServer smpHost smpPort _), ntfQueueId, ntfServer = (NtfServer ntfHost ntfPort _), ntfSubId, ntfSubStatus} action = do
|
||||
ts <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE ntf_subscriptions
|
||||
SET smp_ntf_id = ?, ntf_host = ?, ntf_port = ?, ntf_sub_id = ?, ntf_sub_status = ?, ntf_sub_action = ?, ntf_sub_smp_action = ?, ntf_sub_action_ts = ?, updated_by_supervisor = ?, updated_at = ?
|
||||
SET smp_host = ?, smp_port = ?, smp_ntf_id = ?, ntf_host = ?, ntf_port = ?, ntf_sub_id = ?,
|
||||
ntf_sub_status = ?, ntf_sub_action = ?, ntf_sub_smp_action = ?, ntf_sub_action_ts = ?, updated_by_supervisor = ?, updated_at = ?
|
||||
WHERE conn_id = ?
|
||||
|]
|
||||
((ntfQueueId, ntfHost, ntfPort, ntfSubId) :. (ntfSubStatus, ntfSubAction, ntfSubSMPAction, ts, True, ts, connId))
|
||||
( (smpHost, smpPort, ntfQueueId, ntfHost, ntfPort, ntfSubId)
|
||||
:. (ntfSubStatus, ntfSubAction, ntfSubSMPAction, ts, True, ts, connId)
|
||||
)
|
||||
where
|
||||
(ntfSubAction, ntfSubSMPAction) = ntfSubAndSMPAction action
|
||||
|
||||
@@ -1605,16 +1692,20 @@ deleteNtfSubscription db connId = do
|
||||
WHERE conn_id = ?
|
||||
|]
|
||||
(Nothing :: Maybe SMP.NotifierId, Nothing :: Maybe NtfSubscriptionId, NASDeleted, False, updatedAt, connId)
|
||||
else DB.execute db "DELETE FROM ntf_subscriptions WHERE conn_id = ?" (Only connId)
|
||||
else deleteNtfSubscription' db connId
|
||||
|
||||
getNextNtfSubNTFAction :: DB.Connection -> NtfServer -> IO (Either StoreError (Maybe (NtfSubscription, NtfSubNTFAction, NtfActionTs)))
|
||||
getNextNtfSubNTFAction db ntfServer@(NtfServer ntfHost ntfPort _) =
|
||||
getWorkItem "ntf NTF" getNtfConnId getNtfSubAction (markNtfSubActionNtfFailed_ db)
|
||||
deleteNtfSubscription' :: DB.Connection -> ConnId -> IO ()
|
||||
deleteNtfSubscription' db connId = do
|
||||
DB.execute db "DELETE FROM ntf_subscriptions WHERE conn_id = ?" (Only connId)
|
||||
|
||||
getNextNtfSubNTFActions :: DB.Connection -> NtfServer -> Int -> IO (Either StoreError [Either StoreError (NtfSubNTFAction, NtfSubscription, NtfActionTs)])
|
||||
getNextNtfSubNTFActions db ntfServer@(NtfServer ntfHost ntfPort _) ntfBatchSize =
|
||||
getWorkItems "ntf NTF" getNtfConnIds getNtfSubAction (markNtfSubActionNtfFailed_ db)
|
||||
where
|
||||
getNtfConnId :: IO (Maybe ConnId)
|
||||
getNtfConnId =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
getNtfConnIds :: IO [ConnId]
|
||||
getNtfConnIds =
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT conn_id
|
||||
@@ -1622,10 +1713,10 @@ getNextNtfSubNTFAction db ntfServer@(NtfServer ntfHost ntfPort _) =
|
||||
WHERE ntf_host = ? AND ntf_port = ? AND ntf_sub_action IS NOT NULL
|
||||
AND (ntf_failed = 0 OR updated_by_supervisor = 1)
|
||||
ORDER BY ntf_sub_action_ts ASC
|
||||
LIMIT 1
|
||||
LIMIT ?
|
||||
|]
|
||||
(ntfHost, ntfPort)
|
||||
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubscription, NtfSubNTFAction, NtfActionTs))
|
||||
(ntfHost, ntfPort, ntfBatchSize)
|
||||
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubNTFAction, NtfSubscription, NtfActionTs))
|
||||
getNtfSubAction connId = do
|
||||
markUpdatedByWorker db connId
|
||||
firstRow ntfSubAction err $
|
||||
@@ -1645,20 +1736,20 @@ getNextNtfSubNTFAction db ntfServer@(NtfServer ntfHost ntfPort _) =
|
||||
ntfSubAction (userId, smpHost, smpPort, smpKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
|
||||
let smpServer = SMPServer smpHost smpPort smpKeyHash
|
||||
ntfSubscription = NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
|
||||
in (ntfSubscription, action, actionTs)
|
||||
in (action, ntfSubscription, actionTs)
|
||||
|
||||
markNtfSubActionNtfFailed_ :: DB.Connection -> ConnId -> IO ()
|
||||
markNtfSubActionNtfFailed_ db connId =
|
||||
DB.execute db "UPDATE ntf_subscriptions SET ntf_failed = 1 where conn_id = ?" (Only connId)
|
||||
|
||||
getNextNtfSubSMPAction :: DB.Connection -> SMPServer -> IO (Either StoreError (Maybe (NtfSubscription, NtfSubSMPAction, NtfActionTs)))
|
||||
getNextNtfSubSMPAction db smpServer@(SMPServer smpHost smpPort _) =
|
||||
getWorkItem "ntf SMP" getNtfConnId getNtfSubAction (markNtfSubActionSMPFailed_ db)
|
||||
getNextNtfSubSMPActions :: DB.Connection -> SMPServer -> Int -> IO (Either StoreError [Either StoreError (NtfSubSMPAction, NtfSubscription)])
|
||||
getNextNtfSubSMPActions db smpServer@(SMPServer smpHost smpPort _) ntfBatchSize =
|
||||
getWorkItems "ntf SMP" getNtfConnIds getNtfSubAction (markNtfSubActionSMPFailed_ db)
|
||||
where
|
||||
getNtfConnId :: IO (Maybe ConnId)
|
||||
getNtfConnId =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
getNtfConnIds :: IO [ConnId]
|
||||
getNtfConnIds =
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT conn_id
|
||||
@@ -1666,10 +1757,10 @@ getNextNtfSubSMPAction db smpServer@(SMPServer smpHost smpPort _) =
|
||||
WHERE smp_host = ? AND smp_port = ? AND ntf_sub_smp_action IS NOT NULL AND ntf_sub_action_ts IS NOT NULL
|
||||
AND (smp_failed = 0 OR updated_by_supervisor = 1)
|
||||
ORDER BY ntf_sub_action_ts ASC
|
||||
LIMIT 1
|
||||
LIMIT ?
|
||||
|]
|
||||
(smpHost, smpPort)
|
||||
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubscription, NtfSubSMPAction, NtfActionTs))
|
||||
(smpHost, smpPort, ntfBatchSize)
|
||||
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubSMPAction, NtfSubscription))
|
||||
getNtfSubAction connId = do
|
||||
markUpdatedByWorker db connId
|
||||
firstRow ntfSubAction err $
|
||||
@@ -1677,7 +1768,7 @@ getNextNtfSubSMPAction db smpServer@(SMPServer smpHost smpPort _) =
|
||||
db
|
||||
[sql|
|
||||
SELECT c.user_id, s.ntf_host, s.ntf_port, s.ntf_key_hash,
|
||||
ns.smp_ntf_id, ns.ntf_sub_id, ns.ntf_sub_status, ns.ntf_sub_action_ts, ns.ntf_sub_smp_action
|
||||
ns.smp_ntf_id, ns.ntf_sub_id, ns.ntf_sub_status, ns.ntf_sub_smp_action
|
||||
FROM ntf_subscriptions ns
|
||||
JOIN connections c USING (conn_id)
|
||||
JOIN ntf_servers s USING (ntf_host, ntf_port)
|
||||
@@ -1686,10 +1777,10 @@ getNextNtfSubSMPAction db smpServer@(SMPServer smpHost smpPort _) =
|
||||
(Only connId)
|
||||
where
|
||||
err = SEInternal $ "ntf subscription " <> bshow connId <> " returned []"
|
||||
ntfSubAction (userId, ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
|
||||
ntfSubAction (userId, ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, action) =
|
||||
let ntfServer = NtfServer ntfHost ntfPort ntfKeyHash
|
||||
ntfSubscription = NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
|
||||
in (ntfSubscription, action, actionTs)
|
||||
in (action, ntfSubscription)
|
||||
|
||||
markNtfSubActionSMPFailed_ :: DB.Connection -> ConnId -> IO ()
|
||||
markNtfSubActionSMPFailed_ db connId =
|
||||
@@ -2004,7 +2095,7 @@ setConnDeleted db waitDelivery connId
|
||||
DB.execute db "UPDATE connections SET deleted = ? WHERE conn_id = ?" (True, connId)
|
||||
|
||||
setConnUserId :: DB.Connection -> UserId -> ConnId -> UserId -> IO ()
|
||||
setConnUserId db oldUserId connId newUserId =
|
||||
setConnUserId db oldUserId connId newUserId =
|
||||
DB.execute db "UPDATE connections SET user_id = ? WHERE conn_id = ? and user_id = ?" (newUserId, connId, oldUserId)
|
||||
|
||||
setConnAgentVersion :: DB.Connection -> ConnId -> VersionSMPA -> IO ()
|
||||
|
||||
@@ -74,6 +74,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -116,7 +117,8 @@ schemaMigrations =
|
||||
("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem),
|
||||
("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays),
|
||||
("m20240624_snd_secure", m20240624_snd_secure, Just down_m20240624_snd_secure),
|
||||
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats)
|
||||
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats),
|
||||
("m20240930_ntf_tokens_to_delete", m20240930_ntf_tokens_to_delete, Just down_m20240930_ntf_tokens_to_delete)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20240930_ntf_tokens_to_delete :: Query
|
||||
m20240930_ntf_tokens_to_delete =
|
||||
[sql|
|
||||
CREATE TABLE ntf_tokens_to_delete (
|
||||
ntf_token_to_delete_id INTEGER PRIMARY KEY,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_key_hash BLOB NOT NULL,
|
||||
tkn_id BLOB NOT NULL, -- token ID assigned by notifications server
|
||||
tkn_priv_key BLOB NOT NULL, -- client's private key to sign token commands,
|
||||
del_failed INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|]
|
||||
|
||||
down_m20240930_ntf_tokens_to_delete :: Query
|
||||
down_m20240930_ntf_tokens_to_delete =
|
||||
[sql|
|
||||
DROP TABLE ntf_tokens_to_delete;
|
||||
|]
|
||||
@@ -403,6 +403,16 @@ CREATE TABLE servers_stats(
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE ntf_tokens_to_delete(
|
||||
ntf_token_to_delete_id INTEGER PRIMARY KEY,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_key_hash BLOB NOT NULL,
|
||||
tkn_id BLOB NOT NULL, -- token ID assigned by notifications server
|
||||
tkn_priv_key BLOB NOT NULL, -- client's private key to sign token commands,
|
||||
del_failed INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id);
|
||||
CREATE UNIQUE INDEX idx_rcv_queue_id ON rcv_queues(conn_id, rcv_queue_id);
|
||||
CREATE UNIQUE INDEX idx_snd_queue_id ON snd_queues(conn_id, snd_queue_id);
|
||||
|
||||
@@ -63,11 +63,13 @@ module Simplex.Messaging.Client
|
||||
forwardSMPTransmission,
|
||||
getSMPQueueInfo,
|
||||
sendProtocolCommand,
|
||||
sendProtocolCommands,
|
||||
|
||||
-- * Supporting types and client configuration
|
||||
ProtocolClientError (..),
|
||||
SMPClientError,
|
||||
ProxyClientError (..),
|
||||
Response (..),
|
||||
unexpectedResponse,
|
||||
ProtocolClientConfig (..),
|
||||
NetworkConfig (..),
|
||||
@@ -80,10 +82,11 @@ module Simplex.Messaging.Client
|
||||
defaultSMPClientConfig,
|
||||
defaultNetworkConfig,
|
||||
transportClientConfig,
|
||||
clientSocksCredentials,
|
||||
chooseTransportHost,
|
||||
proxyUsername,
|
||||
temporaryClientError,
|
||||
smpProxyError,
|
||||
textToHostMode,
|
||||
ServerTransmissionBatch,
|
||||
ServerTransmission (..),
|
||||
ClientCommand,
|
||||
@@ -116,16 +119,20 @@ import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import Network.Socket (ServiceName)
|
||||
import Network.Socks5 (SocksCredentials (..))
|
||||
import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
@@ -136,7 +143,7 @@ import Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient)
|
||||
import Simplex.Messaging.Transport.Client (SocksAuth (..), SocksProxyWithAuth (..), TransportClientConfig (..), TransportHost (..), defaultSMPPort, defaultTcpConnectTimeout, runTransportClient)
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM)
|
||||
@@ -236,6 +243,12 @@ data HostMode
|
||||
HMPublic
|
||||
deriving (Eq, Show)
|
||||
|
||||
textToHostMode :: Text -> Either String HostMode
|
||||
textToHostMode = \case
|
||||
"public" -> Right HMPublic
|
||||
"onion" -> Right HMOnionViaSocks
|
||||
s -> Left $ T.unpack $ "Invalid host_mode: " <> s
|
||||
|
||||
data SocksMode
|
||||
= -- | always use SOCKS proxy when enabled
|
||||
SMAlways
|
||||
@@ -257,7 +270,7 @@ instance StrEncoding SocksMode where
|
||||
-- | network configuration for the client
|
||||
data NetworkConfig = NetworkConfig
|
||||
{ -- | use SOCKS5 proxy
|
||||
socksProxy :: Maybe SocksProxy,
|
||||
socksProxy :: Maybe SocksProxyWithAuth,
|
||||
-- | when to use SOCKS proxy
|
||||
socksMode :: SocksMode,
|
||||
-- | determines critera which host is chosen from the list
|
||||
@@ -270,6 +283,8 @@ data NetworkConfig = NetworkConfig
|
||||
smpProxyMode :: SMPProxyMode,
|
||||
-- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions
|
||||
smpProxyFallback :: SMPProxyFallback,
|
||||
-- | use web port 443 for SMP protocol
|
||||
smpWebPort :: Bool,
|
||||
-- | timeout for the initial client TCP/TLS connection (microseconds)
|
||||
tcpConnectTimeout :: Int,
|
||||
-- | timeout of protocol commands (microseconds)
|
||||
@@ -288,7 +303,7 @@ data NetworkConfig = NetworkConfig
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data TransportSessionMode = TSMUser | TSMEntity
|
||||
data TransportSessionMode = TSMUser | TSMSession | TSMServer | TSMEntity
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- SMP proxy mode for sending messages
|
||||
@@ -338,9 +353,10 @@ defaultNetworkConfig =
|
||||
socksMode = SMAlways,
|
||||
hostMode = HMOnionViaSocks,
|
||||
requiredHostMode = False,
|
||||
sessionMode = TSMUser,
|
||||
sessionMode = TSMSession,
|
||||
smpProxyMode = SPMNever,
|
||||
smpProxyFallback = SPFAllow,
|
||||
smpWebPort = False,
|
||||
tcpConnectTimeout = defaultTcpConnectTimeout,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpTimeoutPerKb = 5_000,
|
||||
@@ -351,15 +367,31 @@ defaultNetworkConfig =
|
||||
logTLSErrors = False
|
||||
}
|
||||
|
||||
transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host =
|
||||
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
|
||||
transportClientConfig :: NetworkConfig -> TransportHost -> Bool -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host useSNI =
|
||||
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing, useSNI}
|
||||
where
|
||||
useSocksProxy SMAlways = socksProxy
|
||||
socksProxy' = (\(SocksProxyWithAuth _ proxy) -> proxy) <$> socksProxy
|
||||
useSocksProxy SMAlways = socksProxy'
|
||||
useSocksProxy SMOnion = case host of
|
||||
THOnionHost _ -> socksProxy
|
||||
THOnionHost _ -> socksProxy'
|
||||
_ -> Nothing
|
||||
{-# INLINE transportClientConfig #-}
|
||||
|
||||
clientSocksCredentials :: ProtocolTypeI (ProtoType msg) => NetworkConfig -> UTCTime -> TransportSession msg -> Maybe SocksCredentials
|
||||
clientSocksCredentials NetworkConfig {socksProxy, sessionMode} proxySessTs (userId, srv, entityId_) = case socksProxy of
|
||||
Just (SocksProxyWithAuth auth _) -> case auth of
|
||||
SocksAuthUsername {username, password} -> Just $ SocksCredentials username password
|
||||
SocksAuthNull -> Nothing
|
||||
SocksIsolateByAuth -> Just $ SocksCredentials sessionUsername ""
|
||||
Nothing -> Nothing
|
||||
where
|
||||
sessionUsername =
|
||||
B64.encode $ C.sha256Hash $
|
||||
bshow userId <> case sessionMode of
|
||||
TSMUser -> ""
|
||||
TSMSession -> ":" <> bshow proxySessTs
|
||||
TSMServer -> ":" <> bshow proxySessTs <> "@" <> strEncode srv
|
||||
TSMEntity -> ":" <> bshow proxySessTs <> "@" <> strEncode srv <> maybe "" ("/" <>) entityId_
|
||||
|
||||
-- | protocol client configuration.
|
||||
data ProtocolClientConfig v = ProtocolClientConfig
|
||||
@@ -373,24 +405,29 @@ data ProtocolClientConfig v = ProtocolClientConfig
|
||||
-- | client-server protocol version range
|
||||
serverVRange :: VersionRange v,
|
||||
-- | agree shared session secret (used in SMP proxy for additional encryption layer)
|
||||
agreeSecret :: Bool
|
||||
agreeSecret :: Bool,
|
||||
-- | send SNI to server, False for SMP
|
||||
useSNI :: Bool
|
||||
}
|
||||
|
||||
-- | Default protocol client configuration.
|
||||
defaultClientConfig :: Maybe [ALPN] -> VersionRange v -> ProtocolClientConfig v
|
||||
defaultClientConfig clientALPN serverVRange =
|
||||
defaultClientConfig :: Maybe [ALPN] -> Bool -> VersionRange v -> ProtocolClientConfig v
|
||||
defaultClientConfig clientALPN useSNI serverVRange =
|
||||
ProtocolClientConfig
|
||||
{ qSize = 64,
|
||||
defaultTransport = ("443", transport @TLS),
|
||||
networkConfig = defaultNetworkConfig,
|
||||
clientALPN,
|
||||
serverVRange,
|
||||
agreeSecret = False
|
||||
agreeSecret = False,
|
||||
useSNI
|
||||
}
|
||||
{-# INLINE defaultClientConfig #-}
|
||||
|
||||
defaultSMPClientConfig :: ProtocolClientConfig SMPVersion
|
||||
defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) supportedClientSMPRelayVRange
|
||||
defaultSMPClientConfig =
|
||||
(defaultClientConfig (Just supportedSMPHandshakes) False supportedClientSMPRelayVRange)
|
||||
{defaultTransport = (show defaultSMPPort, transport @TLS)}
|
||||
{-# INLINE defaultSMPClientConfig #-}
|
||||
|
||||
data Request err msg = Request
|
||||
@@ -449,15 +486,15 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe ByteString)
|
||||
--
|
||||
-- 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 (ServerTransmissionBatch v err msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do
|
||||
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret, useSNI} msgQ proxySessTs disconnected = do
|
||||
case chooseTransportHost networkConfig (host srv) of
|
||||
Right useHost ->
|
||||
(getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost)
|
||||
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
|
||||
Left e -> pure $ Left e
|
||||
where
|
||||
NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
|
||||
NetworkConfig {smpWebPort, tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
|
||||
mkProtocolClient :: TransportHost -> UTCTime -> IO (PClient v err msg)
|
||||
mkProtocolClient transportHost ts = do
|
||||
connected <- newTVarIO False
|
||||
@@ -488,10 +525,10 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
runClient (port', ATransport t) useHost c = do
|
||||
cVar <- newEmptyTMVarIO
|
||||
let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN}
|
||||
username = proxyUsername transportSession
|
||||
let tcConfig = (transportClientConfig networkConfig useHost useSNI) {alpn = clientALPN}
|
||||
socksCreds = clientSocksCredentials networkConfig proxySessTs transportSession
|
||||
tId <-
|
||||
runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar)
|
||||
runTransportClient tcConfig socksCreds useHost port' (Just $ keyHash srv) (client t c cVar)
|
||||
`forkFinally` \_ -> void (atomically . tryPutTMVar cVar $ Left PCENetworkError)
|
||||
c_ <- tcpConnectTimeout `timeout` atomically (takeTMVar cVar)
|
||||
case c_ of
|
||||
@@ -501,7 +538,9 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
|
||||
useTransport :: (ServiceName, ATransport)
|
||||
useTransport = case port srv of
|
||||
"" -> defaultTransport cfg
|
||||
"" -> case protocolTypeI @(ProtoType msg) of
|
||||
SPSMP | smpWebPort -> ("443", transport @TLS)
|
||||
_ -> defaultTransport cfg
|
||||
"80" -> ("80", transport @WS)
|
||||
p -> (p, transport @TLS)
|
||||
|
||||
@@ -597,10 +636,6 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
unexpectedResponse :: Show r => r -> ProtocolClientError err
|
||||
unexpectedResponse = PCEUnexpectedResponse . B.pack . take 32 . show
|
||||
|
||||
proxyUsername :: TransportSession msg -> ByteString
|
||||
proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_
|
||||
{-# INLINE proxyUsername #-}
|
||||
|
||||
-- | Disconnects client from the server and terminates client threads.
|
||||
closeProtocolClient :: ProtocolClient v err msg -> IO ()
|
||||
closeProtocolClient = mapM_ (deRefWeak >=> mapM_ killThread) . action
|
||||
|
||||
@@ -76,7 +76,7 @@ data SMPClientAgentConfig = SMPClientAgentConfig
|
||||
defaultSMPClientAgentConfig :: SMPClientAgentConfig
|
||||
defaultSMPClientAgentConfig =
|
||||
SMPClientAgentConfig
|
||||
{ smpCfg = defaultSMPClientConfig {defaultTransport = ("5223", transport @TLS)},
|
||||
{ smpCfg = defaultSMPClientConfig,
|
||||
reconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = second,
|
||||
@@ -95,6 +95,7 @@ defaultSMPClientAgentConfig =
|
||||
data SMPClientAgent = SMPClientAgent
|
||||
{ agentCfg :: SMPClientAgentConfig,
|
||||
active :: TVar Bool,
|
||||
startedAt :: UTCTime,
|
||||
msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg),
|
||||
agentQ :: TBQueue SMPClientAgentEvent,
|
||||
randomDrg :: TVar ChaChaDRG,
|
||||
@@ -111,6 +112,7 @@ type OwnServer = Bool
|
||||
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> IO SMPClientAgent
|
||||
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do
|
||||
active <- newTVarIO True
|
||||
startedAt <- getCurrentTime
|
||||
msgQ <- newTBQueueIO msgQSize
|
||||
agentQ <- newTBQueueIO agentQSize
|
||||
smpClients <- TM.emptyIO
|
||||
@@ -123,6 +125,7 @@ newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg
|
||||
SMPClientAgent
|
||||
{ agentCfg,
|
||||
active,
|
||||
startedAt,
|
||||
msgQ,
|
||||
agentQ,
|
||||
randomDrg,
|
||||
@@ -194,8 +197,8 @@ isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} =
|
||||
|
||||
-- | Run an SMP client for SMPClientVar
|
||||
connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient)
|
||||
connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg} srv v =
|
||||
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected
|
||||
connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, startedAt} srv v =
|
||||
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) startedAt clientDisconnected
|
||||
where
|
||||
clientDisconnected :: SMPClient -> IO ()
|
||||
clientDisconnected smp = do
|
||||
|
||||
@@ -152,6 +152,12 @@ module Simplex.Messaging.Crypto
|
||||
unsafeSbKey,
|
||||
randomSbKey,
|
||||
|
||||
-- * secret_box chains
|
||||
SbChainKey,
|
||||
SbKeyNonce,
|
||||
sbcInit,
|
||||
sbcHkdf,
|
||||
|
||||
-- * pseudo-random bytes
|
||||
randomBytes,
|
||||
|
||||
@@ -198,6 +204,7 @@ import qualified Crypto.Cipher.Types as AES
|
||||
import qualified Crypto.Cipher.XSalsa as XSalsa
|
||||
import qualified Crypto.Error as CE
|
||||
import Crypto.Hash (Digest, SHA256 (..), SHA512 (..), hash, hashDigestSize)
|
||||
import qualified Crypto.KDF.HKDF as H
|
||||
import qualified Crypto.MAC.Poly1305 as Poly1305
|
||||
import qualified Crypto.PubKey.Curve25519 as X25519
|
||||
import qualified Crypto.PubKey.Curve448 as X448
|
||||
@@ -1334,6 +1341,26 @@ unsafeSbKey s = either error id $ sbKey s
|
||||
randomSbKey :: TVar ChaChaDRG -> STM SbKey
|
||||
randomSbKey gVar = SecretBoxKey <$> randomBytes 32 gVar
|
||||
|
||||
newtype SbChainKey = SecretBoxChainKey {unSbChainKey :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
|
||||
sbcInit :: ByteArrayAccess secret => ByteString -> secret -> (SbChainKey, SbChainKey)
|
||||
sbcInit salt secret = (SecretBoxChainKey ck1, SecretBoxChainKey ck2)
|
||||
where
|
||||
prk = H.extract salt secret :: H.PRK SHA512
|
||||
out = H.expand prk ("SimpleXSbChainInit" :: ByteString) 64
|
||||
(ck1, ck2) = B.splitAt 32 out
|
||||
|
||||
type SbKeyNonce = (SbKey, CbNonce)
|
||||
|
||||
sbcHkdf :: SbChainKey -> (SbKeyNonce, SbChainKey)
|
||||
sbcHkdf (SecretBoxChainKey ck) = ((SecretBoxKey sk, CryptoBoxNonce nonce), SecretBoxChainKey ck')
|
||||
where
|
||||
prk = H.extract B.empty ck :: H.PRK SHA512
|
||||
out = H.expand prk ("SimpleXSbChain" :: ByteString) 88 -- = 32 (new chain key) + 32 (secret_box key) + 24 (nonce)
|
||||
(ck', rest) = B.splitAt 32 out
|
||||
(sk, nonce) = B.splitAt 32 rest
|
||||
|
||||
xSalsa20 :: ByteArrayAccess key => key -> ByteString -> ByteString -> (ByteString, ByteString)
|
||||
xSalsa20 secret nonce msg = (rs, msg')
|
||||
where
|
||||
|
||||
@@ -17,6 +17,8 @@ module Simplex.Messaging.Crypto.Lazy
|
||||
kcbEncryptTailTag,
|
||||
sbDecryptTailTag,
|
||||
kcbDecryptTailTag,
|
||||
sbEncryptTailTagNoPad,
|
||||
sbDecryptTailTagNoPad,
|
||||
fastReplicate,
|
||||
secretBox,
|
||||
secretBoxTailTag,
|
||||
@@ -49,7 +51,7 @@ import Data.Composition ((.:.))
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Foreign (sizeOf)
|
||||
import Simplex.Messaging.Crypto (CbNonce, CryptoError (..), DhSecret (..), DhSecretX25519, SbKey, pattern CbNonce, pattern SbKey)
|
||||
import Simplex.Messaging.Crypto (CbNonce, CryptoError (..), DhSecret (..), DhSecretX25519, SbKey, SbKeyNonce, pattern CbNonce, pattern SbKey)
|
||||
import Simplex.Messaging.Crypto.SNTRUP761 (KEMHybridSecret (..))
|
||||
import Simplex.Messaging.Encoding
|
||||
|
||||
@@ -142,6 +144,10 @@ sbEncryptTailTag_ :: ByteArrayAccess key => key -> CbNonce -> LazyByteString ->
|
||||
sbEncryptTailTag_ key (CbNonce nonce) msg len paddedLen =
|
||||
LB.fromChunks <$> (secretBoxTailTag sbEncryptChunk key nonce =<< pad msg len paddedLen)
|
||||
|
||||
sbEncryptTailTagNoPad :: SbKeyNonce -> LazyByteString -> Either CryptoError LazyByteString
|
||||
sbEncryptTailTagNoPad (SbKey key, CbNonce nonce) msg =
|
||||
LB.fromChunks <$> secretBoxTailTag sbEncryptChunk key nonce msg
|
||||
|
||||
-- | NaCl @secret_box@ decrypt with a symmetric 256-bit key and 192-bit nonce with appended auth tag (more efficient with large files).
|
||||
-- paddedLen should NOT include the tag length, it should be the same number that is passed to sbEncrypt / sbEncryptTailTag.
|
||||
sbDecryptTailTag :: SbKey -> CbNonce -> Int64 -> LazyByteString -> Either CryptoError (Bool, LazyByteString)
|
||||
@@ -165,6 +171,15 @@ sbDecryptTailTag_ key (CbNonce nonce) paddedLen packet =
|
||||
where
|
||||
(c, tag') = LB.splitAt paddedLen packet
|
||||
|
||||
sbDecryptTailTagNoPad :: SbKeyNonce -> Int64 -> LazyByteString -> Either CryptoError (Bool, LazyByteString)
|
||||
sbDecryptTailTagNoPad (SbKey key, CbNonce nonce) paddedLen packet =
|
||||
result <$> secretBox sbDecryptChunk key nonce c
|
||||
where
|
||||
result (tag :| cs) =
|
||||
let valid = LB.length tag' == 16 && BA.constEq (LB.toStrict tag') tag
|
||||
in (valid, LB.fromChunks cs)
|
||||
(c, tag') = LB.splitAt paddedLen packet
|
||||
|
||||
secretBoxTailTag :: ByteArrayAccess key => (SbState -> ByteString -> (ByteString, SbState)) -> key -> ByteString -> LazyByteString -> Either CryptoError [ByteString]
|
||||
secretBoxTailTag sbProcess secret nonce msg = run <$> sbInit_ secret nonce
|
||||
where
|
||||
|
||||
@@ -2,24 +2,31 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Client where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Word (Word16)
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes)
|
||||
import Simplex.Messaging.Protocol (ErrorType, pattern NoEntity)
|
||||
import Simplex.Messaging.Transport (TLS, Transport (..))
|
||||
|
||||
type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse
|
||||
|
||||
type NtfClientError = ProtocolClientError ErrorType
|
||||
|
||||
defaultNTFClientConfig :: ProtocolClientConfig NTFVersion
|
||||
defaultNTFClientConfig = defaultClientConfig (Just supportedNTFHandshakes) supportedClientNTFVRange
|
||||
defaultNTFClientConfig =
|
||||
(defaultClientConfig (Just supportedNTFHandshakes) True supportedClientNTFVRange)
|
||||
{defaultTransport = ("443", transport @TLS)}
|
||||
{-# INLINE defaultNTFClientConfig #-}
|
||||
|
||||
ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519)
|
||||
ntfRegisterToken c pKey newTkn =
|
||||
@@ -51,12 +58,30 @@ ntfCreateSubscription c pKey newSub =
|
||||
NRSubId subId -> pure subId
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
ntfCreateSubscriptions :: NtfClient -> C.APrivateAuthKey -> NonEmpty (NewNtfEntity 'Subscription) -> IO (NonEmpty (Either NtfClientError NtfSubscriptionId))
|
||||
ntfCreateSubscriptions c pKey newSubs = L.map process <$> sendProtocolCommands c cs
|
||||
where
|
||||
cs = L.map (\newSub -> (Just pKey, NoEntity, NtfCmd SSubscription $ SNEW newSub)) newSubs
|
||||
process (Response _ r) = case r of
|
||||
Right (NRSubId subId) -> Right subId
|
||||
Right r' -> Left $ unexpectedResponse r'
|
||||
Left e -> Left e
|
||||
|
||||
ntfCheckSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus
|
||||
ntfCheckSubscription c pKey subId =
|
||||
sendNtfCommand c (Just pKey) subId SCHK >>= \case
|
||||
NRSub stat -> pure stat
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
ntfCheckSubscriptions :: NtfClient -> C.APrivateAuthKey -> NonEmpty NtfSubscriptionId -> IO (NonEmpty (Either NtfClientError NtfSubStatus))
|
||||
ntfCheckSubscriptions c pKey subIds = L.map process <$> sendProtocolCommands c cs
|
||||
where
|
||||
cs = L.map (\subId -> (Just pKey, subId, NtfCmd SSubscription SCHK)) subIds
|
||||
process (Response _ r) = case r of
|
||||
Right (NRSub stat) -> Right stat
|
||||
Right r' -> Left $ unexpectedResponse r'
|
||||
Left e -> Left e
|
||||
|
||||
ntfDeleteSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO ()
|
||||
ntfDeleteSubscription = okNtfCommand SDEL
|
||||
|
||||
|
||||
@@ -443,6 +443,8 @@ data NtfSubStatus
|
||||
NSInactive
|
||||
| -- | END received
|
||||
NSEnd
|
||||
| -- | DELD received (connection was deleted)
|
||||
NSDeleted
|
||||
| -- | SMP AUTH error
|
||||
NSAuth
|
||||
| -- | SMP error other than AUTH
|
||||
@@ -456,6 +458,7 @@ ntfShouldSubscribe = \case
|
||||
NSActive -> True
|
||||
NSInactive -> True
|
||||
NSEnd -> False
|
||||
NSDeleted -> False
|
||||
NSAuth -> False
|
||||
NSErr _ -> False
|
||||
|
||||
@@ -466,6 +469,7 @@ instance Encoding NtfSubStatus where
|
||||
NSActive -> "ACTIVE"
|
||||
NSInactive -> "INACTIVE"
|
||||
NSEnd -> "END"
|
||||
NSDeleted -> "DELETED"
|
||||
NSAuth -> "AUTH"
|
||||
NSErr err -> "ERR " <> err
|
||||
smpP =
|
||||
@@ -475,6 +479,7 @@ instance Encoding NtfSubStatus where
|
||||
"ACTIVE" -> pure NSActive
|
||||
"INACTIVE" -> pure NSInactive
|
||||
"END" -> pure NSEnd
|
||||
"DELETED" -> pure NSDeleted
|
||||
"AUTH" -> pure NSAuth
|
||||
"ERR" -> NSErr <$> (A.space *> A.takeByteString)
|
||||
_ -> fail "bad NtfSubStatus"
|
||||
|
||||
@@ -19,6 +19,7 @@ import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.IORef
|
||||
import Data.Int (Int64)
|
||||
@@ -50,8 +51,8 @@ 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 (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..), defaultSupportedParams)
|
||||
import Simplex.Messaging.Transport.Server (AddHTTP, runTransportServer)
|
||||
import Simplex.Messaging.Util
|
||||
import System.Exit (exitFailure)
|
||||
import System.IO (BufferMode (..), hPutStrLn, hSetBuffering)
|
||||
@@ -80,12 +81,12 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
|
||||
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
|
||||
runServer :: (ServiceName, ATransport, AddHTTP) -> M ()
|
||||
runServer (tcpPort, ATransport t, _addHTTP) = do
|
||||
srvCreds <- asks tlsServerCreds
|
||||
serverSignKey <- either fail pure $ fromTLSCredentials srvCreds
|
||||
env <- ask
|
||||
liftIO $ runTransportServer started tcpPort serverParams tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
|
||||
liftIO $ runTransportServer started tcpPort defaultSupportedParams srvCreds (Just supportedNTFHandshakes) tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
|
||||
fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey
|
||||
|
||||
runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M ()
|
||||
@@ -226,6 +227,7 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
Right SMP.END ->
|
||||
whenM (atomically $ activeClientSession' ca sessionId srv) $
|
||||
updateSubStatus smpQueue NSEnd
|
||||
Right SMP.DELD -> updateSubStatus smpQueue NSDeleted
|
||||
Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e
|
||||
Right _ -> logError "SMP server unexpected response"
|
||||
Left e -> logError $ "SMP client error: " <> tshow e
|
||||
@@ -353,28 +355,29 @@ clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connecte
|
||||
|
||||
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
|
||||
atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime
|
||||
logDebug "received transmission"
|
||||
case cmdOrError of
|
||||
Left e -> write sndQ (corrId, entId, NRErr e)
|
||||
Right cmd ->
|
||||
verifyNtfTransmission ((,C.cbNonce (SMP.bs corrId)) <$> thAuth) t cmd >>= \case
|
||||
VRVerified req -> write rcvQ req
|
||||
VRFailed -> write sndQ (corrId, entId, NRErr AUTH)
|
||||
ts <- L.toList <$> liftIO (tGet th)
|
||||
atomically . (writeTVar rcvActiveAt $!) =<< liftIO getSystemTime
|
||||
(errs, cmds) <- partitionEithers <$> mapM cmdAction ts
|
||||
write sndQ errs
|
||||
write rcvQ cmds
|
||||
where
|
||||
write q t = atomically $ writeTBQueue q t
|
||||
cmdAction t@(_, _, (corrId, entId, cmdOrError)) =
|
||||
case cmdOrError of
|
||||
Left e -> pure $ Left (corrId, entId, NRErr e)
|
||||
Right cmd ->
|
||||
verified <$> verifyNtfTransmission ((,C.cbNonce (SMP.bs corrId)) <$> thAuth) t cmd
|
||||
where
|
||||
verified = \case
|
||||
VRVerified req -> Right req
|
||||
VRFailed -> Left (corrId, entId, NRErr AUTH)
|
||||
write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty
|
||||
|
||||
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)]
|
||||
ts <- atomically $ readTBQueue sndQ
|
||||
void . liftIO $ tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts
|
||||
atomically . (writeTVar sndActiveAt $!) =<< liftIO getSystemTime
|
||||
|
||||
-- instance Show a => Show (TVar a) where
|
||||
-- show x = unsafePerformIO $ show <$> readTVarIO x
|
||||
|
||||
data VerificationResult = VRVerified NtfRequest | VRFailed
|
||||
|
||||
verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult
|
||||
@@ -432,7 +435,7 @@ client :: NtfServerClient -> NtfSubscriber -> NtfPushServer -> M ()
|
||||
client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPushServer {pushQ, intervalNotifiers} =
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= processCommand
|
||||
>>= mapM processCommand
|
||||
>>= atomically . writeTBQueue sndQ
|
||||
where
|
||||
processCommand :: NtfRequest -> M (Transmission NtfResponse)
|
||||
|
||||
@@ -33,13 +33,13 @@ import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Transport.Server (AddHTTP, ServerCredentials, TransportServerConfig, loadFingerprint, loadServerCredential)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
|
||||
data NtfServerConfig = NtfServerConfig
|
||||
{ transports :: [(ServiceName, ATransport)],
|
||||
{ transports :: [(ServiceName, ATransport, AddHTTP)],
|
||||
subIdBytes :: Int,
|
||||
regCodeBytes :: Int,
|
||||
clientQSize :: Natural,
|
||||
@@ -50,10 +50,7 @@ data NtfServerConfig = NtfServerConfig
|
||||
subsBatchSize :: Int,
|
||||
inactiveClientExpiration :: Maybe ExpirationConfig,
|
||||
storeLogFile :: Maybe FilePath,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
ntfCredentials :: ServerCredentials,
|
||||
-- stats config - see SMP server config
|
||||
logStatsInterval :: Maybe Int64,
|
||||
logStatsStartTime :: Int64,
|
||||
@@ -77,13 +74,13 @@ data NtfEnv = NtfEnv
|
||||
store :: NtfStore,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
random :: TVar ChaChaDRG,
|
||||
tlsServerParams :: T.ServerParams,
|
||||
tlsServerCreds :: T.Credential,
|
||||
serverIdentity :: C.KeyHash,
|
||||
serverStats :: NtfServerStats
|
||||
}
|
||||
|
||||
newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, ntfCredentials} = do
|
||||
random <- C.newRandom
|
||||
store <- newNtfStore
|
||||
logInfo "restoring subscriptions..."
|
||||
@@ -91,10 +88,10 @@ newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsCo
|
||||
logInfo "restored subscriptions"
|
||||
subscriber <- newNtfSubscriber subQSize smpAgentCfg random
|
||||
pushServer <- newNtfPushServer pushQSize apnsConfig
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
tlsServerCreds <- loadServerCredential ntfCredentials
|
||||
Fingerprint fp <- loadFingerprint ntfCredentials
|
||||
serverStats <- newNtfServerStats =<< getCurrentTime
|
||||
pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
|
||||
pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
|
||||
|
||||
data NtfSubscriber = NtfSubscriber
|
||||
{ smpSubscribers :: TMap SMPServer SMPSubscriber,
|
||||
@@ -158,8 +155,8 @@ data NtfRequest
|
||||
| NtfReqPing CorrId NtfEntityId
|
||||
|
||||
data NtfServerClient = NtfServerClient
|
||||
{ rcvQ :: TBQueue NtfRequest,
|
||||
sndQ :: TBQueue (Transmission NtfResponse),
|
||||
{ rcvQ :: TBQueue (NonEmpty NtfRequest),
|
||||
sndQ :: TBQueue (NonEmpty (Transmission NtfResponse)),
|
||||
ntfThParams :: THandleParams NTFVersion 'TServer,
|
||||
connected :: TVar Bool,
|
||||
rcvActiveAt :: TVar SystemTime,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Main where
|
||||
|
||||
@@ -16,19 +17,19 @@ import qualified Data.Text as T
|
||||
import qualified Data.Text.IO as T
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Server (runNtfServer)
|
||||
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration)
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig)
|
||||
import Simplex.Messaging.Notifications.Transport (supportedNTFHandshakes, supportedServerNTFVRange)
|
||||
import Simplex.Messaging.Notifications.Transport (supportedServerNTFVRange)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer)
|
||||
import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
import System.FilePath (combine)
|
||||
@@ -85,13 +86,19 @@ ntfServerCLI cfgPath logPath =
|
||||
<> ("enable: " <> onOff enableStoreLog <> "\n\n")
|
||||
<> "log_stats: off\n\n\
|
||||
\[TRANSPORT]\n\
|
||||
\# host is only used to print server address on start\n"
|
||||
\# Host is only used to print server address on start.\n\
|
||||
\# You can specify multiple server ports.\n"
|
||||
<> ("host: " <> T.pack host <> "\n")
|
||||
<> ("port: " <> T.pack defaultServerPort <> "\n")
|
||||
<> "log_tls_errors: off\n"
|
||||
<> "websockets: off\n\n\
|
||||
<> "log_tls_errors: off\n\n\
|
||||
\# Use `websockets: 443` to run websockets server in addition to plain TLS.\n\
|
||||
\websockets: off\n\n\
|
||||
\[SUBSCRIBER]\n\
|
||||
\# Network configuration for notification server client.\n\
|
||||
\# `host_mode` can be 'public' (default) or 'onion'.\n\
|
||||
\# It defines prefferred hostname for destination servers with multiple hostnames.\n\
|
||||
\# host_mode: public\n\
|
||||
\# required_host_mode: off\n\n\
|
||||
\# SOCKS proxy port for subscribing to SMP servers.\n\
|
||||
\# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\
|
||||
\# socks_proxy: localhost:9050\n\n\
|
||||
@@ -134,6 +141,8 @@ ntfServerCLI cfgPath logPath =
|
||||
defaultNetworkConfig
|
||||
{ socksProxy = either error id <$!> strDecodeIni "SUBSCRIBER" "socks_proxy" ini,
|
||||
socksMode = maybe SMOnion (either error id) $! strDecodeIni "SUBSCRIBER" "socks_mode" ini,
|
||||
hostMode = either (const HMPublic) (either error id . textToHostMode) $ lookupValue "SUBSCRIBER" "host_mode" ini,
|
||||
requiredHostMode = fromMaybe False $ iniOnOff "SUBSCRIBER" "required_host_mode" ini,
|
||||
smpPingInterval = 60_000_000 -- 1 minutes
|
||||
}
|
||||
},
|
||||
@@ -148,9 +157,12 @@ ntfServerCLI cfgPath logPath =
|
||||
checkInterval = readStrictIni "INACTIVE_CLIENTS" "check_interval" ini
|
||||
},
|
||||
storeLogFile = enableStoreLog $> storeLogFilePath,
|
||||
caCertificateFile = c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile,
|
||||
ntfCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just $ c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile
|
||||
},
|
||||
logStatsInterval = logStats $> 86400, -- seconds
|
||||
logStatsStartTime = 0, -- seconds from 00:00 UTC
|
||||
serverStatsLogFile = combine logPath "ntf-server-stats.daily.log",
|
||||
@@ -158,8 +170,7 @@ ntfServerCLI cfgPath logPath =
|
||||
ntfServerVRange = supportedServerNTFVRange,
|
||||
transportConfig =
|
||||
defaultTransportServerConfig
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
|
||||
alpn = Just supportedNTFHandshakes
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ data NtfStoreLogRecord
|
||||
| CreateSubscription NtfSubRec
|
||||
| SubscriptionStatus NtfSubscriptionId NtfSubStatus
|
||||
| DeleteSubscription NtfSubscriptionId
|
||||
deriving (Show)
|
||||
|
||||
data NtfTknRec = NtfTknRec
|
||||
{ ntfTknId :: NtfTokenId,
|
||||
@@ -61,6 +62,7 @@ data NtfTknRec = NtfTknRec
|
||||
tknRegCode :: NtfRegCode,
|
||||
tknCronInterval :: Word16
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
mkTknData :: NtfTknRec -> STM NtfTknData
|
||||
mkTknData NtfTknRec {ntfTknId, token, tknStatus = status, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval = cronInt} = do
|
||||
@@ -81,6 +83,7 @@ data NtfSubRec = NtfSubRec
|
||||
tokenId :: NtfTokenId,
|
||||
subStatus :: NtfSubStatus
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
mkSubData :: NtfSubRec -> STM NtfSubData
|
||||
mkSubData NtfSubRec {ntfSubId, smpQueue, notifierKey, tokenId, subStatus = status} = do
|
||||
|
||||
@@ -99,8 +99,8 @@ type NtfActionTs = UTCTime
|
||||
data NtfSubNTFAction
|
||||
= NSACreate
|
||||
| NSACheck
|
||||
| NSADelete
|
||||
| NSARotate
|
||||
| NSADelete -- deprecated
|
||||
| NSARotate -- deprecated
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding NtfSubNTFAction where
|
||||
|
||||
@@ -484,6 +484,7 @@ data BrokerMsg where
|
||||
RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy
|
||||
PRES :: EncResponse -> BrokerMsg -- proxy to client
|
||||
END :: BrokerMsg
|
||||
DELD :: BrokerMsg
|
||||
INFO :: QueueInfo -> BrokerMsg
|
||||
OK :: BrokerMsg
|
||||
ERR :: ErrorType -> BrokerMsg
|
||||
@@ -705,6 +706,7 @@ data BrokerMsgTag
|
||||
| RRES_
|
||||
| PRES_
|
||||
| END_
|
||||
| DELD_
|
||||
| INFO_
|
||||
| OK_
|
||||
| ERR_
|
||||
@@ -778,6 +780,7 @@ instance Encoding BrokerMsgTag where
|
||||
RRES_ -> "RRES"
|
||||
PRES_ -> "PRES"
|
||||
END_ -> "END"
|
||||
DELD_ -> "DELD"
|
||||
INFO_ -> "INFO"
|
||||
OK_ -> "OK"
|
||||
ERR_ -> "ERR"
|
||||
@@ -794,6 +797,7 @@ instance ProtocolMsgTag BrokerMsgTag where
|
||||
"RRES" -> Just RRES_
|
||||
"PRES" -> Just PRES_
|
||||
"END" -> Just END_
|
||||
"DELD" -> Just DELD_
|
||||
"INFO" -> Just INFO_
|
||||
"OK" -> Just OK_
|
||||
"ERR" -> Just ERR_
|
||||
@@ -1274,7 +1278,7 @@ 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 (ProtocolTypeI (ProtoType msg), 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 -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c 'TClient)
|
||||
@@ -1423,6 +1427,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock)
|
||||
PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock)
|
||||
END -> e END_
|
||||
DELD
|
||||
| v >= deletedEventSMPVersion -> e DELD_
|
||||
| otherwise -> e END_
|
||||
INFO info -> e (INFO_, ' ', info)
|
||||
OK -> e OK_
|
||||
ERR err -> e (ERR_, ' ', err)
|
||||
@@ -1448,6 +1455,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP)
|
||||
PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP)
|
||||
END_ -> pure END
|
||||
DELD_ -> pure DELD
|
||||
INFO_ -> INFO <$> _smpP
|
||||
OK_ -> pure OK
|
||||
ERR_ -> ERR <$> _smpP
|
||||
|
||||
+115
-66
@@ -34,6 +34,7 @@ module Simplex.Messaging.Server
|
||||
verifyCmdAuthorization,
|
||||
dummyVerifyCmd,
|
||||
randomId,
|
||||
AttachHTTP,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -68,10 +69,12 @@ import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (cast)
|
||||
import GHC.IORef (atomicSwapIORef)
|
||||
import GHC.Stats (getRTSStats)
|
||||
import GHC.TypeLits (KnownNat)
|
||||
import Network.Socket (ServiceName, Socket, socketToHandle)
|
||||
import qualified Network.TLS as TLS
|
||||
import Numeric.Natural (Natural)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, forwardSMPTransmission, smpProxyError, temporaryClientError)
|
||||
@@ -115,42 +118,56 @@ import GHC.Conc.Sync (threadLabel)
|
||||
-- | Runs an SMP server using passed configuration.
|
||||
--
|
||||
-- See a full server here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs
|
||||
runSMPServer :: ServerConfig -> IO ()
|
||||
runSMPServer cfg = do
|
||||
runSMPServer :: ServerConfig -> Maybe AttachHTTP -> IO ()
|
||||
runSMPServer cfg attachHTTP_ = do
|
||||
started <- newEmptyTMVarIO
|
||||
runSMPServerBlocking started cfg
|
||||
runSMPServerBlocking started cfg attachHTTP_
|
||||
|
||||
-- | Runs an SMP server using passed configuration with signalling.
|
||||
--
|
||||
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
|
||||
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
|
||||
runSMPServerBlocking :: TMVar Bool -> ServerConfig -> IO ()
|
||||
runSMPServerBlocking started cfg = newEnv cfg >>= runReaderT (smpServer started cfg)
|
||||
runSMPServerBlocking :: TMVar Bool -> ServerConfig -> Maybe AttachHTTP -> IO ()
|
||||
runSMPServerBlocking started cfg attachHTTP_ = newEnv cfg >>= runReaderT (smpServer started cfg attachHTTP_)
|
||||
|
||||
type M a = ReaderT Env IO a
|
||||
type AttachHTTP = Socket -> TLS.Context -> IO ()
|
||||
|
||||
smpServer :: TMVar Bool -> ServerConfig -> M ()
|
||||
smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
smpServer :: TMVar Bool -> ServerConfig -> Maybe AttachHTTP -> M ()
|
||||
smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} attachHTTP_ = do
|
||||
s <- asks server
|
||||
pa <- asks proxyAgent
|
||||
expired <- restoreServerMessages
|
||||
restoreServerStats expired
|
||||
raceAny_
|
||||
( serverThread s "server subscribedQ" subscribedQ subscribers pendingENDs subscriptions cancelSub
|
||||
: serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers pendingNtfENDs ntfSubscriptions (\_ -> pure ())
|
||||
: sendPendingENDsThread s
|
||||
( serverThread s "server subscribedQ" subscribedQ subscribers subClients pendingSubEvents subscriptions cancelSub
|
||||
: serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubClients pendingNtfSubEvents ntfSubscriptions (\_ -> pure ())
|
||||
: sendPendingEvtsThread s
|
||||
: receiveFromProxyAgent pa
|
||||
: map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg
|
||||
)
|
||||
`finally` withLock' (savingLock s) "final" (saveServer False >> closeServer)
|
||||
where
|
||||
runServer :: (ServiceName, ATransport) -> M ()
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
serverParams <- asks tlsServerParams
|
||||
runServer :: (ServiceName, ATransport, AddHTTP) -> M ()
|
||||
runServer (tcpPort, ATransport t, addHTTP) = do
|
||||
smpCreds <- asks tlsServerCreds
|
||||
httpCreds_ <- asks httpServerCreds
|
||||
ss <- asks sockets
|
||||
serverSignKey <- either fail pure . fromTLSCredentials $ tlsServerCredentials serverParams
|
||||
serverSignKey <- either fail pure $ fromTLSCredentials smpCreds
|
||||
env <- ask
|
||||
liftIO $ runTransportServerState ss started tcpPort serverParams tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
|
||||
liftIO $ case (httpCreds_, attachHTTP_) of
|
||||
(Just httpCreds, Just attachHTTP) | addHTTP ->
|
||||
runTransportServerState_ ss started tcpPort defaultSupportedParamsHTTPS chooseCreds (Just combinedALPNs) tCfg $ \s h ->
|
||||
case cast h of
|
||||
Just TLS {tlsContext} | maybe False (`elem` httpALPN) (getSessionALPN h) -> labelMyThread "https client" >> attachHTTP s tlsContext
|
||||
_ -> runClient serverSignKey t h `runReaderT` env
|
||||
where
|
||||
chooseCreds = maybe smpCreds (\_host -> httpCreds)
|
||||
combinedALPNs = supportedSMPHandshakes <> httpALPN
|
||||
httpALPN :: [ALPN]
|
||||
httpALPN = ["h2", "http/1.1"]
|
||||
_ ->
|
||||
runTransportServerState ss started tcpPort defaultSupportedParams smpCreds (Just supportedSMPHandshakes) tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
|
||||
fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey
|
||||
|
||||
saveServer :: Bool -> M ()
|
||||
@@ -165,11 +182,12 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
String ->
|
||||
(Server -> TQueue (QueueId, ClientId, Subscribed)) ->
|
||||
(Server -> TMap QueueId (TVar Client)) ->
|
||||
(Server -> TVar (IM.IntMap (NonEmpty RecipientId))) ->
|
||||
(Server -> TVar (IM.IntMap Client)) ->
|
||||
(Server -> TVar (IM.IntMap (NonEmpty (QueueId, Subscribed)))) ->
|
||||
(Client -> TMap QueueId s) ->
|
||||
(s -> IO ()) ->
|
||||
M ()
|
||||
serverThread s label subQ subs ends clientSubs unsub = do
|
||||
serverThread s label subQ subs subClnts pendingEvts clientSubs unsub = do
|
||||
labelMyThread label
|
||||
cls <- asks clients
|
||||
liftIO . forever $
|
||||
@@ -177,61 +195,72 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
$>>= endPreviousSubscriptions
|
||||
>>= mapM_ unsub
|
||||
where
|
||||
updateSubscribers :: TVar (IM.IntMap (Maybe Client)) -> (QueueId, ClientId, Bool) -> STM (Maybe (QueueId, Client))
|
||||
updateSubscribers :: TVar (IM.IntMap (Maybe Client)) -> (QueueId, ClientId, Subscribed) -> STM (Maybe ((QueueId, Subscribed), Client))
|
||||
updateSubscribers cls (qId, clntId, subscribed) =
|
||||
-- Client lookup by ID is in the same STM transaction.
|
||||
-- In case client disconnects during the transaction,
|
||||
-- it will be re-evaluated, and the client won't be stored as subscribed.
|
||||
(readTVar cls >>= updateSub (subs s) . IM.lookup clntId)
|
||||
(readTVar cls >>= updateSub . IM.lookup clntId)
|
||||
$>>= clientToBeNotified
|
||||
where
|
||||
updateSub ss = \case
|
||||
ss = subs s
|
||||
updateSub = \case
|
||||
Just (Just clnt)
|
||||
| subscribed ->
|
||||
| subscribed -> do
|
||||
modifyTVar' (subClnts s) $ IM.insert clntId clnt -- add client to server's subscribed cients
|
||||
TM.lookup qId ss >>= -- insert subscribed and current client
|
||||
maybe
|
||||
(newTVar clnt >>= \cv -> TM.insert qId cv ss $> Nothing)
|
||||
(\cv -> Just <$> swapTVar cv clnt)
|
||||
| otherwise -> TM.lookupDelete qId ss >>= mapM readTVar
|
||||
| otherwise -> do
|
||||
removeWhenNoSubs clnt
|
||||
TM.lookupDelete qId ss >>= mapM readTVar
|
||||
-- This case catches Just Nothing - it cannot happen here.
|
||||
-- Nothing is there only before client thread is started.
|
||||
_ -> TM.lookup qId ss >>= mapM readTVar -- do not insert client if it is already disconnected, but send END to any other client
|
||||
clientToBeNotified c'
|
||||
| clntId == clientId c' = pure Nothing
|
||||
| otherwise = (\yes -> if yes then Just (qId, c') else Nothing) <$> readTVar (connected c')
|
||||
endPreviousSubscriptions :: (QueueId, Client) -> IO (Maybe s)
|
||||
endPreviousSubscriptions (qId, c) = do
|
||||
atomically $ modifyTVar' (ends s) $ IM.alter (Just . maybe [qId] (qId <|)) (clientId c)
|
||||
atomically $ TM.lookupDelete qId (clientSubs c)
|
||||
| otherwise = (\yes -> if yes then Just ((qId, subscribed), c') else Nothing) <$> readTVar (connected c')
|
||||
endPreviousSubscriptions :: ((QueueId, Subscribed), Client) -> IO (Maybe s)
|
||||
endPreviousSubscriptions (qEvt@(qId, _), c) = do
|
||||
atomically $ modifyTVar' (pendingEvts s) $ IM.alter (Just . maybe [qEvt] (qEvt <|)) (clientId c)
|
||||
atomically $ do
|
||||
sub <- TM.lookupDelete qId (clientSubs c)
|
||||
removeWhenNoSubs c $> sub
|
||||
-- remove client from server's subscribed cients
|
||||
removeWhenNoSubs c = whenM (null <$> readTVar (clientSubs c)) $ modifyTVar' (subClnts s) $ IM.delete (clientId c)
|
||||
|
||||
sendPendingENDsThread :: Server -> M ()
|
||||
sendPendingENDsThread s = do
|
||||
sendPendingEvtsThread :: Server -> M ()
|
||||
sendPendingEvtsThread s = do
|
||||
endInt <- asks $ pendingENDInterval . config
|
||||
cls <- asks clients
|
||||
forever $ do
|
||||
threadDelay endInt
|
||||
sendPending cls $ pendingENDs s
|
||||
sendPending cls $ pendingNtfENDs s
|
||||
sendPending cls $ pendingSubEvents s
|
||||
sendPending cls $ pendingNtfSubEvents s
|
||||
where
|
||||
sendPending cls ref = do
|
||||
ends <- atomically $ swapTVar ref IM.empty
|
||||
unless (null ends) $ forM_ (IM.assocs ends) $ \(cId, qIds) ->
|
||||
mapM_ (queueENDs qIds) . join . IM.lookup cId =<< readTVarIO cls
|
||||
queueENDs qIds c@Client {connected, sndQ = q} =
|
||||
unless (null ends) $ forM_ (IM.assocs ends) $ \(cId, qEvts) ->
|
||||
mapM_ (queueEvts qEvts) . join . IM.lookup cId =<< readTVarIO cls
|
||||
queueEvts qEvts c@Client {connected, sndQ = q} =
|
||||
whenM (readTVarIO connected) $ do
|
||||
sent <- atomically $ ifM (isFullTBQueue q) (pure False) (writeTBQueue q ts $> True)
|
||||
if sent
|
||||
then updateEndStats
|
||||
else -- if queue is full it can block
|
||||
forkClient c ("sendPendingENDsThread.queueENDs") $
|
||||
forkClient c ("sendPendingEvtsThread.queueEvts") $
|
||||
atomically (writeTBQueue q ts) >> updateEndStats
|
||||
where
|
||||
ts = L.map (CorrId "",,END) qIds
|
||||
ts = L.map (\(qId, subscribed) -> (CorrId "", qId, evt subscribed)) qEvts
|
||||
evt True = END
|
||||
evt False = DELD
|
||||
-- this accounts for both END and DELD events
|
||||
updateEndStats = do
|
||||
stats <- asks serverStats
|
||||
let len = L.length qIds
|
||||
let len = L.length qEvts
|
||||
liftIO $ atomicModifyIORef'_ (qSubEnd stats) (+ len)
|
||||
liftIO $ atomicModifyIORef'_ (qSubEndB stats) (+ (len `div` 255 + 1)) -- up to 255 ENDs in the batch
|
||||
liftIO $ atomicModifyIORef'_ (qSubEndB stats) (+ (len `div` 255 + 1)) -- up to 255 ENDs or DELDs in the batch
|
||||
|
||||
receiveFromProxyAgent :: ProxyAgent -> M ()
|
||||
receiveFromProxyAgent ProxyAgent {smpAgent = SMPClientAgent {agentQ}} =
|
||||
@@ -246,16 +275,16 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
showServer' = decodeLatin1 . strEncode . host
|
||||
|
||||
expireMessagesThread_ :: ServerConfig -> [M ()]
|
||||
expireMessagesThread_ ServerConfig {messageExpiration = Just msgExp} = [expireMessages msgExp]
|
||||
expireMessagesThread_ ServerConfig {messageExpiration = Just msgExp} = [expireMessagesThread msgExp]
|
||||
expireMessagesThread_ _ = []
|
||||
|
||||
expireMessages :: ExpirationConfig -> M ()
|
||||
expireMessages expCfg = do
|
||||
expireMessagesThread :: ExpirationConfig -> M ()
|
||||
expireMessagesThread expCfg = do
|
||||
ms <- asks msgStore
|
||||
quota <- asks $ msgQueueQuota . config
|
||||
let interval = checkInterval expCfg * 1000000
|
||||
stats <- asks serverStats
|
||||
labelMyThread "expireMessages"
|
||||
labelMyThread "expireMessagesThread"
|
||||
forever $ do
|
||||
liftIO $ threadDelay' interval
|
||||
old <- liftIO $ expireBeforeEpoch expCfg
|
||||
@@ -557,7 +586,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
#else
|
||||
hPutStrLn h "Threads: not available on GHC 8.10"
|
||||
#endif
|
||||
Env {clients, server = Server {subscribers, notifiers}} <- unliftIO u ask
|
||||
Env {clients, server = Server {subscribers, notifiers, subClients, ntfSubClients}} <- unliftIO u ask
|
||||
activeClients <- readTVarIO clients
|
||||
hPutStrLn h $ "Clients: " <> show (IM.size activeClients)
|
||||
when (r == CPRAdmin) $ do
|
||||
@@ -574,6 +603,8 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
hPutStrLn h $ "Ntf subscribed clients queues (via clients, rcvQ, sndQ, msgQ): " <> show ntfClQs
|
||||
putActiveClientsInfo "SMP" subscribers
|
||||
putActiveClientsInfo "Ntf" notifiers
|
||||
putSubscribedClients "SMP" subClients
|
||||
putSubscribedClients "Ntf" ntfSubClients
|
||||
where
|
||||
putActiveClientsInfo :: String -> TMap QueueId (TVar Client) -> IO ()
|
||||
putActiveClientsInfo protoName clients = do
|
||||
@@ -584,6 +615,10 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
where
|
||||
countSubClients :: M.Map QueueId (TVar Client) -> IO IS.IntSet
|
||||
countSubClients = foldM (\ !s c -> (`IS.insert` s) . clientId <$> readTVarIO c) IS.empty
|
||||
putSubscribedClients :: String -> TVar (IM.IntMap Client) -> IO ()
|
||||
putSubscribedClients protoName subClnts = do
|
||||
clnts <- readTVarIO subClnts
|
||||
hPutStrLn h $ protoName <> " subscribed clients count:" <> show (IM.size clnts)
|
||||
countClientSubs :: (Client -> TMap QueueId a) -> Maybe (M.Map QueueId a -> IO (Int, Int, Int, Int)) -> IM.IntMap (Maybe Client) -> IO (Int, (Int, Int, Int, Int), Int, (Natural, Natural, Natural))
|
||||
countClientSubs subSel countSubs_ = foldM addSubs (0, (0, 0, 0, 0), 0, (0, 0, 0))
|
||||
where
|
||||
@@ -673,11 +708,14 @@ runClientTransport h@THandle {params = thParams@THandleParams {thVersion, sessio
|
||||
expCfg <- asks $ inactiveClientExpiration . config
|
||||
th <- newMVar h -- put TH under a fair lock to interleave messages and command responses
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId
|
||||
raceAny_ $ [liftIO $ send th c, liftIO $ sendMsg th c, client thParams c s, receive h c] <> disconnectThread_ c expCfg
|
||||
disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport h (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)]
|
||||
disconnectThread_ _ _ = []
|
||||
noSubscriptions c = atomically $ (&&) <$> TM.null (ntfSubscriptions c) <*> (not . hasSubs <$> readTVar (subscriptions c))
|
||||
hasSubs = any $ (\case ServerSub _ -> True; ProhibitSub -> False) . subThread
|
||||
raceAny_ $ [liftIO $ send th c, liftIO $ sendMsg th c, client thParams c s, receive h c] <> disconnectThread_ c s expCfg
|
||||
disconnectThread_ c s (Just expCfg) = [liftIO $ disconnectTransport h (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c s)]
|
||||
disconnectThread_ _ _ _ = []
|
||||
noSubscriptions Client {clientId} s = do
|
||||
hasSubs <- IM.member clientId <$> readTVarIO (subClients s)
|
||||
if hasSubs
|
||||
then pure False
|
||||
else not . IM.member clientId <$> readTVarIO (ntfSubClients s)
|
||||
|
||||
clientDisconnected :: Client -> M ()
|
||||
clientDisconnected c@Client {clientId, subscriptions, ntfSubscriptions, connected, sessionId, endThreads} = do
|
||||
@@ -688,10 +726,12 @@ clientDisconnected c@Client {clientId, subscriptions, ntfSubscriptions, connecte
|
||||
subs <- atomically $ swapTVar subscriptions M.empty
|
||||
ntfSubs <- atomically $ swapTVar ntfSubscriptions M.empty
|
||||
liftIO $ mapM_ cancelSub subs
|
||||
Server {subscribers, notifiers} <- asks server
|
||||
Server {subscribers, notifiers, subClients, ntfSubClients} <- asks server
|
||||
liftIO $ updateSubscribers subs subscribers
|
||||
liftIO $ updateSubscribers ntfSubs notifiers
|
||||
asks clients >>= atomically . (`modifyTVar'` IM.delete clientId)
|
||||
atomically $ modifyTVar' subClients $ IM.delete clientId
|
||||
atomically $ modifyTVar' ntfSubClients $ IM.delete clientId
|
||||
tIds <- atomically $ swapTVar endThreads IM.empty
|
||||
liftIO $ mapM_ (mapM_ killThread <=< deRefWeak) tIds
|
||||
where
|
||||
@@ -780,7 +820,7 @@ send th c@Client {sndQ, msgQ, sessionId} = do
|
||||
-- replace MSG response with OK, accumulating MSG in a separate list.
|
||||
MSG {} -> ((CorrId "", entId, cmd) : msgs, (corrId, entId, OK))
|
||||
_ -> (msgs, t)
|
||||
|
||||
|
||||
sendMsg :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO ()
|
||||
sendMsg th c@Client {msgQ, sessionId} = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " sendMsg"
|
||||
@@ -985,11 +1025,9 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
processCommand (qr_, (corrId, entId, cmd)) = case cmd of
|
||||
Cmd SProxiedClient command -> processProxiedCmd (corrId, entId, command)
|
||||
Cmd SSender command -> Just <$> case command of
|
||||
SKEY sKey -> (corrId,entId,) <$> case qr_ of
|
||||
Just qr@QueueRec {sndSecure}
|
||||
| sndSecure -> secureQueue_ "SKEY" qr sKey
|
||||
| otherwise -> pure $ ERR AUTH
|
||||
Nothing -> pure $ ERR INTERNAL
|
||||
SKEY sKey ->
|
||||
withQueue $ \QueueRec {sndSecure, recipientId} ->
|
||||
(corrId,entId,) <$> if sndSecure then secureQueue_ "SKEY" recipientId sKey else pure $ ERR AUTH
|
||||
SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody
|
||||
PING -> pure (corrId, NoEntity, PONG)
|
||||
RFWD encBlock -> (corrId, NoEntity,) <$> processForwardedCommand encBlock
|
||||
@@ -1009,9 +1047,9 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
SUB -> withQueue (`subscribeQueue` entId)
|
||||
GET -> withQueue getMessage
|
||||
ACK msgId -> withQueue (`acknowledgeMsg` msgId)
|
||||
KEY sKey -> (corrId,entId,) <$> case qr_ of
|
||||
Just qr -> secureQueue_ "KEY" qr sKey
|
||||
Nothing -> pure $ ERR INTERNAL
|
||||
KEY sKey ->
|
||||
withQueue $ \QueueRec {recipientId} ->
|
||||
(corrId,entId,) <$> secureQueue_ "KEY" recipientId sKey
|
||||
NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey
|
||||
NDEL -> deleteQueueNotifier_ st
|
||||
OFF -> suspendQueue_ st
|
||||
@@ -1063,10 +1101,9 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
n <- asks $ queueIdBytes . config
|
||||
liftM2 (,) (randomId n) (randomId n)
|
||||
|
||||
secureQueue_ :: T.Text -> QueueRec -> SndPublicAuthKey -> M BrokerMsg
|
||||
secureQueue_ name qr@QueueRec {recipientId = rId} sKey = time name $ do
|
||||
secureQueue_ :: T.Text -> RecipientId -> SndPublicAuthKey -> M BrokerMsg
|
||||
secureQueue_ name rId sKey = time name $ do
|
||||
withLog $ \s -> logSecureQueue s rId sKey
|
||||
updateQueueDate qr
|
||||
st <- asks queueStore
|
||||
stats <- asks serverStats
|
||||
incStat $ qSecured stats
|
||||
@@ -1086,20 +1123,22 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
liftIO (addQueueNotifier st entId ntfCreds) >>= \case
|
||||
Left DUPLICATE_ -> addNotifierRetry (n - 1) rcvPublicDhKey rcvNtfDhSecret
|
||||
Left e -> pure $ ERR e
|
||||
Right _ -> do
|
||||
Right nId_ -> do
|
||||
withLog $ \s -> logAddNotifier s entId ntfCreds
|
||||
incStat . ntfCreated =<< asks serverStats
|
||||
forM_ nId_ $ \nId -> atomically $ writeTQueue ntfSubscribedQ (nId, clientId, False)
|
||||
pure $ NID notifierId rcvPublicDhKey
|
||||
|
||||
deleteQueueNotifier_ :: QueueStore -> M (Transmission BrokerMsg)
|
||||
deleteQueueNotifier_ st = do
|
||||
withLog (`logDeleteNotifier` entId)
|
||||
liftIO (deleteQueueNotifier st entId) >>= \case
|
||||
Right () -> do
|
||||
Right (Just nId) -> do
|
||||
-- Possibly, the same should be done if the queue is suspended, but currently we do not use it
|
||||
atomically $ writeTQueue ntfSubscribedQ (entId, clientId, False)
|
||||
atomically $ writeTQueue ntfSubscribedQ (nId, clientId, False)
|
||||
incStat . ntfDeleted =<< asks serverStats
|
||||
pure ok
|
||||
Right Nothing -> pure ok
|
||||
Left e -> pure $ err e
|
||||
|
||||
suspendQueue_ :: QueueStore -> M (Transmission BrokerMsg)
|
||||
@@ -1157,6 +1196,10 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
newSub = do
|
||||
s <- newProhibitedSub
|
||||
TM.insert entId s subscriptions
|
||||
-- Here we don't account for this client as subscribed in the server
|
||||
-- and don't notify other subscribed clients.
|
||||
-- This is tracked as "subscription" in the client to prevent these
|
||||
-- clients from being able to subscribe.
|
||||
pure s
|
||||
getMessage_ :: Sub -> Maybe MsgId -> M (Transmission BrokerMsg)
|
||||
getMessage_ s delivered_ = do
|
||||
@@ -1185,7 +1228,7 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
when (Just t /= updatedAt) $ do
|
||||
withLog $ \s -> logUpdateQueueTime s rId t
|
||||
st <- asks queueStore
|
||||
liftIO $ updateQueueTime st rId t
|
||||
liftIO $ updateQueueTime st rId t
|
||||
|
||||
subscribeNotifications :: M (Transmission BrokerMsg)
|
||||
subscribeNotifications = do
|
||||
@@ -1480,8 +1523,14 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s
|
||||
liftIO (deleteQueue st entId $>>= \q -> delMsgQueue ms entId $> Right q) >>= \case
|
||||
Right q -> do
|
||||
-- Possibly, the same should be done if the queue is suspended, but currently we do not use it
|
||||
atomically $ writeTQueue subscribedQ (entId, clientId, False)
|
||||
atomically $ do
|
||||
writeTQueue subscribedQ (entId, clientId, False)
|
||||
-- queue is usually deleted by the same client that is currently subscribed,
|
||||
-- we delete subscription here, so the client with no subscriptions can be disconnected.
|
||||
TM.delete entId subscriptions
|
||||
forM_ (notifierId <$> notifier q) $ \nId ->
|
||||
-- queue is deleted by a different client from the one subscribed to notifications,
|
||||
-- so we don't need to remove subscription from the current client.
|
||||
atomically $ writeTQueue ntfSubscribedQ (nId, clientId, False)
|
||||
updateDeletedStats q
|
||||
pure ok
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Server.CLI where
|
||||
@@ -16,6 +18,7 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (fromRight)
|
||||
import Data.Ini (Ini, lookupValue)
|
||||
import Data.List ((\\))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
@@ -27,7 +30,7 @@ import Options.Applicative
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI)
|
||||
import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..))
|
||||
import Simplex.Messaging.Transport.Server (loadFingerprint)
|
||||
import Simplex.Messaging.Transport.Server (AddHTTP, loadFileFingerprint)
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, whenM)
|
||||
import System.Directory (doesDirectoryExist, listDirectory, removeDirectoryRecursive, removePathForcibly)
|
||||
@@ -137,7 +140,7 @@ createServerX509_ createCA cfgPath x509cfg = do
|
||||
)
|
||||
|
||||
saveFingerprint = do
|
||||
Fingerprint fp <- loadFingerprint $ c caCrtFile
|
||||
Fingerprint fp <- loadFileFingerprint $ c caCrtFile
|
||||
withFile (c fingerprintFile) WriteMode (`B.hPutStrLn` strEncode fp)
|
||||
pure fp
|
||||
|
||||
@@ -266,26 +269,37 @@ settingIsOn section name ini
|
||||
checkSavedFingerprint :: FilePath -> X509Config -> IO ByteString
|
||||
checkSavedFingerprint cfgPath x509cfg = do
|
||||
savedFingerprint <- withFile (c fingerprintFile) ReadMode hGetLine
|
||||
Fingerprint fp <- loadFingerprint (c caCrtFile)
|
||||
Fingerprint fp <- loadFileFingerprint (c caCrtFile)
|
||||
when (B.pack savedFingerprint /= strEncode fp) $
|
||||
exitError "Stored fingerprint is invalid."
|
||||
pure fp
|
||||
where
|
||||
c = combine cfgPath . ($ x509cfg)
|
||||
|
||||
iniTransports :: Ini -> [(String, ATransport)]
|
||||
iniTransports :: Ini -> [(ServiceName, ATransport, AddHTTP)]
|
||||
iniTransports ini =
|
||||
let port = T.unpack $ strictIni "TRANSPORT" "port" ini
|
||||
enableWebsockets = (== "on") $ strictIni "TRANSPORT" "websockets" ini
|
||||
in (port, transport @TLS) : [("80", transport @WS) | enableWebsockets]
|
||||
let smpPorts = ports $ strictIni "TRANSPORT" "port" ini
|
||||
ws = strictIni "TRANSPORT" "websockets" ini
|
||||
wsPorts
|
||||
| ws == "off" = []
|
||||
| ws == "on" = ["80"]
|
||||
| otherwise = ports ws \\ smpPorts
|
||||
in ts (transport @TLS) smpPorts <> ts (transport @WS) wsPorts
|
||||
where
|
||||
ts :: ATransport -> [ServiceName] -> [(ServiceName, ATransport, AddHTTP)]
|
||||
ts t = map (\port -> (port, t, webPort == Just port))
|
||||
webPort = T.unpack <$> eitherToMaybe (lookupValue "WEB" "https" ini)
|
||||
ports = map T.unpack . T.splitOn ","
|
||||
|
||||
printServerConfig :: [(ServiceName, ATransport)] -> Maybe FilePath -> IO ()
|
||||
printServerConfig :: [(ServiceName, ATransport, AddHTTP)] -> Maybe FilePath -> IO ()
|
||||
printServerConfig transports logFile = do
|
||||
putStrLn $ case logFile of
|
||||
Just f -> "Store log: " <> f
|
||||
_ -> "Store log disabled."
|
||||
forM_ transports $ \(p, ATransport t) ->
|
||||
putStrLn $ "Listening on port " <> p <> " (" <> transportName t <> ")..."
|
||||
forM_ transports $ \(p, ATransport t, addHTTP) -> do
|
||||
let descr = p <> " (" <> transportName t <> ")..."
|
||||
putStrLn $ "Serving SMP protocol on port " <> descr
|
||||
when addHTTP $ putStrLn $ "Serving static site on port " <> descr
|
||||
|
||||
deleteDirIfExists :: FilePath -> IO ()
|
||||
deleteDirIfExists path = whenM (doesDirectoryExist path) $ removeDirectoryRecursive path
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
@@ -10,11 +11,13 @@ module Simplex.Messaging.Server.Env.STM where
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import qualified Crypto.PubKey.RSA as RSA
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.IntMap.Strict (IntMap)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import Data.List (intercalate)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
@@ -22,6 +25,7 @@ import Data.Maybe (isJust, isNothing)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import qualified Data.X509 as X
|
||||
import Data.X509.Validation (Fingerprint (..))
|
||||
import Network.Socket (ServiceName)
|
||||
import qualified Network.TLS as T
|
||||
@@ -41,13 +45,15 @@ import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP)
|
||||
import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams, newSocketState)
|
||||
import Simplex.Messaging.Transport.Server
|
||||
import System.Directory (doesFileExist)
|
||||
import System.Exit (exitFailure)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
|
||||
data ServerConfig = ServerConfig
|
||||
{ transports :: [(ServiceName, ATransport)],
|
||||
{ transports :: [(ServiceName, ATransport, AddHTTP)],
|
||||
smpHandshakeTimeout :: Int,
|
||||
tbqSize :: Natural,
|
||||
msgQueueQuota :: Int,
|
||||
@@ -78,10 +84,8 @@ data ServerConfig = ServerConfig
|
||||
serverStatsBackupFile :: Maybe FilePath,
|
||||
-- | interval between sending pending END events to unsubscribed clients, seconds
|
||||
pendingENDInterval :: Int,
|
||||
-- | CA certificate private key is not needed for initialization
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
smpCredentials :: ServerCredentials,
|
||||
httpCredentials :: Maybe ServerCredentials,
|
||||
-- | SMP client-server protocol version range
|
||||
smpServerVRange :: VersionRangeSMP,
|
||||
-- | TCP transport config
|
||||
@@ -125,7 +129,8 @@ data Env = Env
|
||||
msgStore :: STMMsgStore,
|
||||
random :: TVar ChaChaDRG,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
tlsServerParams :: T.ServerParams,
|
||||
tlsServerCreds :: T.Credential,
|
||||
httpServerCreds :: Maybe T.Credential,
|
||||
serverStats :: ServerStats,
|
||||
sockets :: SocketState,
|
||||
clientSeq :: TVar ClientId,
|
||||
@@ -140,8 +145,10 @@ data Server = Server
|
||||
subscribers :: TMap RecipientId (TVar Client),
|
||||
ntfSubscribedQ :: TQueue (NotifierId, ClientId, Subscribed),
|
||||
notifiers :: TMap NotifierId (TVar Client),
|
||||
pendingENDs :: TVar (IntMap (NonEmpty RecipientId)),
|
||||
pendingNtfENDs :: TVar (IntMap (NonEmpty NotifierId)),
|
||||
subClients :: TVar (IntMap Client), -- clients with SMP subscriptions
|
||||
ntfSubClients :: TVar (IntMap Client), -- clients with Ntf subscriptions
|
||||
pendingSubEvents :: TVar (IntMap (NonEmpty (RecipientId, Subscribed))),
|
||||
pendingNtfSubEvents :: TVar (IntMap (NonEmpty (NotifierId, Subscribed))),
|
||||
savingLock :: Lock
|
||||
}
|
||||
|
||||
@@ -184,10 +191,12 @@ newServer = do
|
||||
subscribers <- TM.emptyIO
|
||||
ntfSubscribedQ <- newTQueueIO
|
||||
notifiers <- TM.emptyIO
|
||||
pendingENDs <- newTVarIO IM.empty
|
||||
pendingNtfENDs <- newTVarIO IM.empty
|
||||
subClients <- newTVarIO IM.empty
|
||||
ntfSubClients <- newTVarIO IM.empty
|
||||
pendingSubEvents <- newTVarIO IM.empty
|
||||
pendingNtfSubEvents <- newTVarIO IM.empty
|
||||
savingLock <- atomically createLock
|
||||
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, pendingENDs, pendingNtfENDs, savingLock}
|
||||
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, subClients, ntfSubClients, pendingSubEvents, pendingNtfSubEvents, savingLock}
|
||||
|
||||
newClient :: ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> IO Client
|
||||
newClient clientId qSize thVersion sessionId createdAt = do
|
||||
@@ -216,7 +225,7 @@ newProhibitedSub = do
|
||||
return Sub {subThread = ProhibitSub, delivered}
|
||||
|
||||
newEnv :: ServerConfig -> IO Env
|
||||
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg, transportConfig, information, messageExpiration} = do
|
||||
newEnv config@ServerConfig {smpCredentials, httpCredentials, storeLogFile, smpAgentCfg, information, messageExpiration} = do
|
||||
server <- newServer
|
||||
queueStore <- newQueueStore
|
||||
msgStore <- newMsgStore
|
||||
@@ -225,16 +234,38 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile,
|
||||
forM storeLogFile $ \f -> do
|
||||
logInfo $ "restoring queues from file " <> T.pack f
|
||||
restoreQueues queueStore f
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
tlsServerCreds <- getCredentials "SMP" smpCredentials
|
||||
httpServerCreds <- mapM (getCredentials "HTTPS") httpCredentials
|
||||
mapM_ checkHTTPSCredentials httpServerCreds
|
||||
Fingerprint fp <- loadFingerprint smpCredentials
|
||||
let serverIdentity = KeyHash fp
|
||||
serverStats <- newServerStats =<< getCurrentTime
|
||||
sockets <- newSocketState
|
||||
clientSeq <- newTVarIO 0
|
||||
clients <- newTVarIO mempty
|
||||
proxyAgent <- newSMPProxyAgent smpAgentCfg random
|
||||
pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent}
|
||||
pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerCreds, httpServerCreds, serverStats, sockets, clientSeq, clients, proxyAgent}
|
||||
where
|
||||
getCredentials protocol creds = do
|
||||
files <- missingCreds
|
||||
unless (null files) $ do
|
||||
putStrLn $ "Error: no " <> protocol <> " credentials: " <> intercalate ", " files
|
||||
when (protocol == "HTTPS") $ putStrLn letsEncrypt
|
||||
exitFailure
|
||||
loadServerCredential creds
|
||||
where
|
||||
missingfile f = (\y -> [f | not y]) <$> doesFileExist f
|
||||
missingCreds = do
|
||||
let files = maybe id (:) (caCertificateFile creds) [certificateFile creds, privateKeyFile creds]
|
||||
in concat <$> mapM missingfile files
|
||||
checkHTTPSCredentials (X.CertificateChain cc, _k) =
|
||||
-- LetsEncrypt provides ECDSA with insecure curve p256 (https://safecurves.cr.yp.to)
|
||||
case map (X.signedObject . X.getSigned) cc of
|
||||
X.Certificate {X.certPubKey = X.PubKeyRSA rsa} : _ca | RSA.public_size rsa >= 512 -> pure ()
|
||||
_ -> do
|
||||
putStrLn $ "Error: unsupported HTTPS credentials, required 4096-bit RSA\n" <> letsEncrypt
|
||||
exitFailure
|
||||
letsEncrypt = "Use Let's Encrypt to generate: certbot certonly --standalone -d yourdomainname --key-type rsa --rsa-key-size 4096"
|
||||
restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode)
|
||||
restoreQueues QueueStore {queues, senders, notifiers} f = do
|
||||
(qs, s) <- readWriteStoreLog f
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.Messaging.Server.Main where
|
||||
|
||||
@@ -29,20 +30,20 @@ import qualified Data.Text.IO as T
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Agent.Protocol (connReqUriP')
|
||||
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer)
|
||||
import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server (AttachHTTP, runSMPServer)
|
||||
import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration, defaultProxyClientConcurrency)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.Information
|
||||
import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedServerSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow)
|
||||
import Simplex.Messaging.Version (mkVersionRange)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
@@ -51,10 +52,16 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
smpServerCLI :: FilePath -> FilePath -> IO ()
|
||||
smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ())
|
||||
smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ()) (\_ -> error "attachStaticFiles not available")
|
||||
|
||||
smpServerCLI_ :: (ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) -> (EmbeddedWebParams -> IO ()) -> FilePath -> FilePath -> IO ()
|
||||
smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
smpServerCLI_ ::
|
||||
(ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) ->
|
||||
(EmbeddedWebParams -> IO ()) ->
|
||||
(FilePath -> (AttachHTTP -> IO ()) -> IO ()) ->
|
||||
FilePath ->
|
||||
FilePath ->
|
||||
IO ()
|
||||
smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
|
||||
getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case
|
||||
Init opts ->
|
||||
doesFileExist iniFile >>= \case
|
||||
@@ -76,10 +83,10 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
where
|
||||
iniFile = combine cfgPath "smp-server.ini"
|
||||
serverVersion = "SMP server v" <> simplexMQVersion
|
||||
defaultServerPort = "5223"
|
||||
defaultServerPorts = "5223,443"
|
||||
executableName = "smp-server"
|
||||
storeLogFilePath = combine logPath "smp-server-store.log"
|
||||
httpsCertFile = combine cfgPath "web.cert"
|
||||
httpsCertFile = combine cfgPath "web.crt"
|
||||
httpsKeyFile = combine cfgPath "web.key"
|
||||
defaultStaticPath = combine logPath "www"
|
||||
initializeServer opts@InitOptions {ip, fqdn, sourceCode = src', webStaticPath = sp', disableWeb = noWeb', scripted}
|
||||
@@ -95,7 +102,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine
|
||||
sourceCode' <- withPrompt ("Enter server source code URI (" <> maybe simplexmqSource T.unpack src' <> "): ") getServerSourceCode
|
||||
staticPath' <- withPrompt ("Enter path to store generated static site with server information (" <> fromMaybe defaultStaticPath sp' <> "): ") getLine
|
||||
enableWeb <- onOffPrompt "Enable built-in web server for static site" (not noWeb')
|
||||
initialize
|
||||
opts
|
||||
{ enableStoreLog,
|
||||
@@ -104,7 +110,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
password,
|
||||
sourceCode = (T.pack <$> sourceCode') <|> src',
|
||||
webStaticPath = if null staticPath' then sp' else Just staticPath',
|
||||
disableWeb = not enableWeb
|
||||
disableWeb = noWeb'
|
||||
}
|
||||
where
|
||||
serverPassword =
|
||||
@@ -168,10 +174,12 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
\# control_port_admin_password:\n\
|
||||
\# control_port_user_password:\n\n\
|
||||
\[TRANSPORT]\n\
|
||||
\# host is only used to print server address on start\n"
|
||||
\# Host is only used to print server address on start.\n\
|
||||
\# You can specify multiple server ports.\n"
|
||||
<> ("host: " <> T.pack host <> "\n")
|
||||
<> ("port: " <> T.pack defaultServerPort <> "\n")
|
||||
<> "log_tls_errors: off\n\
|
||||
<> ("port: " <> T.pack defaultServerPorts <> "\n")
|
||||
<> "log_tls_errors: off\n\n\
|
||||
\# Use `websockets: 443` to run websockets server in addition to plain TLS.\n\
|
||||
\websockets: off\n\
|
||||
\# control_port: 5224\n\n\
|
||||
\[PROXY]\n\
|
||||
@@ -202,19 +210,21 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
<> "# Run an embedded server on this port\n\
|
||||
\# Onion sites can use any port and register it in the hidden service config.\n\
|
||||
\# Running on a port 80 may require setting process capabilities.\n"
|
||||
<> ((if disableWeb then "# " else "") <> "http: 8000\n\n")
|
||||
<> (webDisabled <> "http: 8000\n\n")
|
||||
<> "# You can run an embedded TLS web server too if you provide port and cert and key files.\n\
|
||||
\# Not required for running relay on onion address.\n\
|
||||
\# https: 443\n"
|
||||
<> ("# cert: " <> T.pack httpsCertFile <> "\n")
|
||||
<> ("# key: " <> T.pack httpsKeyFile <> "\n")
|
||||
\# Not required for running relay on onion address.\n"
|
||||
<> (webDisabled <> "https: 443\n")
|
||||
<> (webDisabled <> "cert: " <> T.pack httpsCertFile <> "\n")
|
||||
<> (webDisabled <> "key: " <> T.pack httpsKeyFile <> "\n")
|
||||
where
|
||||
webDisabled = if disableWeb then "# " else ""
|
||||
runServer ini = do
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
fp <- checkSavedFingerprint cfgPath defaultX509Config
|
||||
let host = either (const "<hostnames>") T.unpack $ lookupValue "TRANSPORT" "host" ini
|
||||
port = T.unpack $ strictIni "TRANSPORT" "port" ini
|
||||
cfg@ServerConfig {information, transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig
|
||||
cfg@ServerConfig {information, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig
|
||||
sourceCode' = (\ServerPublicInfo {sourceCode} -> sourceCode) <$> information
|
||||
srv = ProtoServerWithAuth (SMPServer [THDomainName host] (if port == "5223" then "" else port) (C.KeyHash fp)) newQueueBasicAuth
|
||||
printServiceInfo serverVersion srv
|
||||
@@ -244,23 +254,37 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
newQueuesAllowed = allowNewQueues cfg,
|
||||
basicAuthEnabled = isJust newQueueBasicAuth
|
||||
}
|
||||
runWebServer ini ServerInformation {config, information}
|
||||
runSMPServer cfg
|
||||
case webStaticPath' of
|
||||
Just path | sharedHTTP -> do
|
||||
runWebServer path Nothing ServerInformation {config, information}
|
||||
attachStaticFiles path $ \attachHTTP -> runSMPServer cfg $ Just attachHTTP
|
||||
Just path -> do
|
||||
runWebServer path webHttpsParams' ServerInformation {config, information}
|
||||
runSMPServer cfg Nothing
|
||||
Nothing -> do
|
||||
logWarn "No server static path set"
|
||||
runSMPServer cfg Nothing
|
||||
where
|
||||
enableStoreLog = settingIsOn "STORE_LOG" "enable" ini
|
||||
logStats = settingIsOn "STORE_LOG" "log_stats" ini
|
||||
c = combine cfgPath . ($ defaultX509Config)
|
||||
transports = iniTransports ini
|
||||
sharedHTTP = any (\(_, _, addHTTP) -> addHTTP) transports
|
||||
serverConfig =
|
||||
ServerConfig
|
||||
{ transports = iniTransports ini,
|
||||
{ transports,
|
||||
smpHandshakeTimeout = 120000000,
|
||||
tbqSize = 64,
|
||||
tbqSize = 128,
|
||||
msgQueueQuota = 128,
|
||||
queueIdBytes = 24,
|
||||
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
|
||||
caCertificateFile = c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile,
|
||||
smpCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just $ c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile
|
||||
},
|
||||
httpCredentials = (\WebHttpsParams {key, cert} -> ServerCredentials {caCertificateFile = Nothing, privateKeyFile = key, certificateFile = cert}) <$> webHttpsParams',
|
||||
storeLogFile = enableStoreLog $> storeLogFilePath,
|
||||
storeMsgsFile =
|
||||
let messagesPath = combine logPath "smp-server-messages.log"
|
||||
@@ -293,8 +317,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig =
|
||||
defaultTransportServerConfig
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
|
||||
alpn = Just supportedSMPHandshakes
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
|
||||
},
|
||||
controlPort = eitherToMaybe $ T.unpack <$> lookupValue "TRANSPORT" "control_port" ini,
|
||||
smpAgentCfg =
|
||||
@@ -307,7 +330,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
defaultNetworkConfig
|
||||
{ socksProxy = either error id <$!> strDecodeIni "PROXY" "socks_proxy" ini,
|
||||
socksMode = maybe SMOnion (either error id) $! strDecodeIni "PROXY" "socks_mode" ini,
|
||||
hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini,
|
||||
hostMode = either (const HMPublic) (either error id . textToHostMode) $ lookupValue "PROXY" "host_mode" ini,
|
||||
requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini
|
||||
}
|
||||
},
|
||||
@@ -318,33 +341,25 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini,
|
||||
information = serverPublicInfo ini
|
||||
}
|
||||
textToHostMode :: Text -> HostMode
|
||||
textToHostMode = \case
|
||||
"public" -> HMPublic
|
||||
"onion" -> HMOnionViaSocks
|
||||
s -> error . T.unpack $ "Invalid host_mode: " <> s
|
||||
textToOwnServers :: Text -> [ByteString]
|
||||
textToOwnServers = map encodeUtf8 . T.words
|
||||
|
||||
runWebServer ini si =
|
||||
case eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini of
|
||||
Nothing -> logWarn "No server static path set"
|
||||
Just webStaticPath -> do
|
||||
runWebServer webStaticPath webHttpsParams si = do
|
||||
let onionHost =
|
||||
either (const Nothing) (find isOnion) $
|
||||
strDecode @(L.NonEmpty TransportHost) . encodeUtf8 =<< lookupValue "TRANSPORT" "host" ini
|
||||
webHttpPort = eitherToMaybe $ read . T.unpack <$> lookupValue "WEB" "http" ini
|
||||
webHttpsParams =
|
||||
eitherToMaybe $ do
|
||||
port <- read . T.unpack <$> lookupValue "WEB" "https" ini
|
||||
cert <- T.unpack <$> lookupValue "WEB" "cert" ini
|
||||
key <- T.unpack <$> lookupValue "WEB" "key" ini
|
||||
pure WebHttpsParams {port, cert, key}
|
||||
generateSite si onionHost webStaticPath
|
||||
when (isJust webHttpPort || isJust webHttpsParams) $
|
||||
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams}
|
||||
where
|
||||
isOnion = \case THOnionHost _ -> True; _ -> False
|
||||
webHttpsParams' =
|
||||
eitherToMaybe $ do
|
||||
port <- read . T.unpack <$> lookupValue "WEB" "https" ini
|
||||
cert <- T.unpack <$> lookupValue "WEB" "cert" ini
|
||||
key <- T.unpack <$> lookupValue "WEB" "key" ini
|
||||
pure WebHttpsParams {port, cert, key}
|
||||
webStaticPath' = eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini
|
||||
|
||||
data EmbeddedWebParams = EmbeddedWebParams
|
||||
{ webStaticPath :: FilePath,
|
||||
|
||||
@@ -74,23 +74,26 @@ secureQueue QueueStore {queues} rId sKey = toResult <$> do
|
||||
let !q' = q {senderKey = Just sKey}
|
||||
in writeTVar qVar q' $> Just q'
|
||||
|
||||
addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> IO (Either ErrorType QueueRec)
|
||||
addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> IO (Either ErrorType (Maybe NotifierId))
|
||||
addQueueNotifier QueueStore {queues, notifiers} rId ntfCreds@NtfCreds {notifierId = nId} = do
|
||||
ifM (TM.memberIO nId notifiers) (pure $ Left DUPLICATE_) $
|
||||
withQueue rId queues $ \qVar -> do
|
||||
TM.lookupIO rId queues >>= \case
|
||||
Just qVar -> atomically $ ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $ do
|
||||
q <- readTVar qVar
|
||||
forM_ (notifier q) $ (`TM.delete` notifiers) . notifierId
|
||||
nId_ <- forM (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers $> notifierId
|
||||
let !q' = q {notifier = Just ntfCreds}
|
||||
writeTVar qVar q'
|
||||
TM.insert nId rId notifiers
|
||||
pure q'
|
||||
pure $ Right nId_
|
||||
Nothing -> pure $ Left AUTH
|
||||
|
||||
deleteQueueNotifier :: QueueStore -> RecipientId -> IO (Either ErrorType ())
|
||||
deleteQueueNotifier :: QueueStore -> RecipientId -> IO (Either ErrorType (Maybe NotifierId))
|
||||
deleteQueueNotifier QueueStore {queues, notifiers} rId =
|
||||
withQueue rId queues $ \qVar -> do
|
||||
q <- readTVar qVar
|
||||
forM_ (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers
|
||||
writeTVar qVar $! q {notifier = Nothing}
|
||||
forM (notifier q) $ \NtfCreds {notifierId} -> do
|
||||
TM.delete notifierId notifiers
|
||||
writeTVar qVar $! q {notifier = Nothing}
|
||||
pure notifierId
|
||||
|
||||
suspendQueue :: QueueStore -> RecipientId -> IO (Either ErrorType ())
|
||||
suspendQueue QueueStore {queues} rId =
|
||||
|
||||
@@ -55,6 +55,7 @@ data StoreLogRecord
|
||||
| DeleteQueue QueueId
|
||||
| DeleteNotifier QueueId
|
||||
| UpdateTime QueueId RoundedSystemTime
|
||||
deriving (Show)
|
||||
|
||||
data SLRTag
|
||||
= CreateQueue_
|
||||
@@ -74,10 +75,11 @@ instance StrEncoding QueueRec where
|
||||
"sid=" <> strEncode senderId,
|
||||
"sk=" <> strEncode senderKey
|
||||
]
|
||||
<> if sndSecure then " sndSecure=" <> strEncode sndSecure else ""
|
||||
<> sndSecureStr
|
||||
<> maybe "" notifierStr notifier
|
||||
<> maybe "" updatedAtStr updatedAt
|
||||
where
|
||||
sndSecureStr = if sndSecure then " sndSecure=" <> strEncode sndSecure else ""
|
||||
notifierStr ntfCreds = " notifier=" <> strEncode ntfCreds
|
||||
updatedAtStr t = " updated_at=" <> strEncode t
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ module Simplex.Messaging.Transport
|
||||
authCmdsSMPVersion,
|
||||
sendingProxySMPVersion,
|
||||
sndAuthKeySMPVersion,
|
||||
deletedEventSMPVersion,
|
||||
simplexMQVersion,
|
||||
smpBlockSize,
|
||||
TransportConfig (..),
|
||||
@@ -64,13 +65,15 @@ module Simplex.Messaging.Transport
|
||||
ALPN,
|
||||
connectTLS,
|
||||
closeTLS,
|
||||
supportedParameters,
|
||||
defaultSupportedParams,
|
||||
defaultSupportedParamsHTTPS,
|
||||
withTlsUnique,
|
||||
|
||||
-- * SMP transport
|
||||
THandle (..),
|
||||
THandleParams (..),
|
||||
THandleAuth (..),
|
||||
TSbChainKeys (..),
|
||||
TransportError (..),
|
||||
HandshakeError (..),
|
||||
smpServerHandshake,
|
||||
@@ -84,6 +87,7 @@ module Simplex.Messaging.Transport
|
||||
where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (forM, (<$!>))
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except (throwE)
|
||||
@@ -97,6 +101,7 @@ import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Default (def)
|
||||
import Data.Functor (($>))
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Version (showVersion)
|
||||
import Data.Word (Word16)
|
||||
import qualified Data.X509 as X
|
||||
@@ -130,6 +135,9 @@ smpBlockSize = 16384
|
||||
-- 5 - basic auth for SMP servers (11/12/2022)
|
||||
-- 6 - allow creating queues without subscribing (9/10/2023)
|
||||
-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (4/30/2024)
|
||||
-- 8 - SMP proxy for sender commands
|
||||
-- 9 - faster handshake: SKEY command for sender to secure queue
|
||||
-- 10 - DELD event to subscriber when queue is deleted via another connnection
|
||||
|
||||
data SMPVersion
|
||||
|
||||
@@ -160,14 +168,17 @@ sendingProxySMPVersion = VersionSMP 8
|
||||
sndAuthKeySMPVersion :: VersionSMP
|
||||
sndAuthKeySMPVersion = VersionSMP 9
|
||||
|
||||
deletedEventSMPVersion :: VersionSMP
|
||||
deletedEventSMPVersion = VersionSMP 10
|
||||
|
||||
currentClientSMPRelayVersion :: VersionSMP
|
||||
currentClientSMPRelayVersion = VersionSMP 9
|
||||
currentClientSMPRelayVersion = VersionSMP 10
|
||||
|
||||
legacyServerSMPRelayVersion :: VersionSMP
|
||||
legacyServerSMPRelayVersion = VersionSMP 6
|
||||
|
||||
currentServerSMPRelayVersion :: VersionSMP
|
||||
currentServerSMPRelayVersion = VersionSMP 9
|
||||
currentServerSMPRelayVersion = VersionSMP 10
|
||||
|
||||
-- Max SMP protocol version to be used in e2e encrypted
|
||||
-- connection between client and server, as defined by SMP proxy.
|
||||
@@ -205,7 +216,7 @@ data TransportConfig = TransportConfig
|
||||
transportTimeout :: Maybe Int
|
||||
}
|
||||
|
||||
class Transport c where
|
||||
class Typeable c => Transport c where
|
||||
transport :: ATransport
|
||||
transport = ATransport (TProxy @c)
|
||||
|
||||
@@ -303,8 +314,8 @@ closeTLS ctx =
|
||||
`E.finally` T.contextClose ctx
|
||||
`catchAll_` pure ()
|
||||
|
||||
supportedParameters :: T.Supported
|
||||
supportedParameters =
|
||||
defaultSupportedParams :: T.Supported
|
||||
defaultSupportedParams =
|
||||
def
|
||||
{ T.supportedVersions = [T.TLS13, T.TLS12],
|
||||
T.supportedCiphers =
|
||||
@@ -312,8 +323,29 @@ supportedParameters =
|
||||
TE.cipher_ECDHE_ECDSA_CHACHA20POLY1305_SHA256 -- for TLS12
|
||||
],
|
||||
T.supportedHashSignatures = [(T.HashIntrinsic, T.SignatureEd448), (T.HashIntrinsic, T.SignatureEd25519)],
|
||||
T.supportedSecureRenegotiation = False,
|
||||
T.supportedGroups = [T.X448, T.X25519]
|
||||
T.supportedGroups = [T.X448, T.X25519],
|
||||
T.supportedSecureRenegotiation = False
|
||||
}
|
||||
|
||||
-- | A selection of extra parameters to accomodate browser chains
|
||||
defaultSupportedParamsHTTPS :: T.Supported
|
||||
defaultSupportedParamsHTTPS =
|
||||
defaultSupportedParams
|
||||
{ T.supportedCiphers = TE.ciphersuite_strong,
|
||||
T.supportedGroups = [T.X25519, T.X448, T.FFDHE4096, T.FFDHE6144, T.FFDHE8192, T.P521],
|
||||
T.supportedHashSignatures =
|
||||
[ (T.HashIntrinsic, T.SignatureEd448),
|
||||
(T.HashIntrinsic, T.SignatureEd25519),
|
||||
(T.HashSHA256, T.SignatureECDSA),
|
||||
(T.HashSHA384, T.SignatureECDSA),
|
||||
(T.HashSHA512, T.SignatureECDSA),
|
||||
(T.HashIntrinsic, T.SignatureRSApssRSAeSHA512),
|
||||
(T.HashIntrinsic, T.SignatureRSApssRSAeSHA384),
|
||||
(T.HashIntrinsic, T.SignatureRSApssRSAeSHA256),
|
||||
(T.HashSHA512, T.SignatureRSA),
|
||||
(T.HashSHA384, T.SignatureRSA),
|
||||
(T.HashSHA256, T.SignatureRSA)
|
||||
]
|
||||
}
|
||||
|
||||
instance Transport TLS where
|
||||
@@ -386,6 +418,11 @@ data THandleAuth (p :: TransportPeer) where
|
||||
} ->
|
||||
THandleAuth 'TServer
|
||||
|
||||
data TSbChainKeys = TSbChainKeys
|
||||
{ sndKey :: TVar C.SbChainKey,
|
||||
rcvKey :: TVar C.SbChainKey
|
||||
}
|
||||
|
||||
-- | TLS-unique channel binding
|
||||
type SessionId = ByteString
|
||||
|
||||
|
||||
@@ -13,9 +13,13 @@ module Simplex.Messaging.Transport.Client
|
||||
defaultSMPPort,
|
||||
defaultTcpConnectTimeout,
|
||||
defaultTransportClientConfig,
|
||||
defaultSocksProxyWithAuth,
|
||||
defaultSocksProxy,
|
||||
defaultSocksHost,
|
||||
TransportClientConfig (..),
|
||||
SocksProxy,
|
||||
SocksProxy (..),
|
||||
SocksProxyWithAuth (..),
|
||||
SocksAuth (..),
|
||||
TransportHost (..),
|
||||
TransportHosts (..),
|
||||
TransportHosts_ (..),
|
||||
@@ -23,7 +27,7 @@ module Simplex.Messaging.Transport.Client
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad (when)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
@@ -79,7 +83,7 @@ instance StrEncoding TransportHost where
|
||||
strP =
|
||||
A.choice
|
||||
[ THIPv4 <$> ((,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal),
|
||||
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> A.takeWhile1 (\c -> isHexDigit c || c == ':'),
|
||||
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> ipv6StrP,
|
||||
THOnionHost <$> ((<>) <$> A.takeWhile (\c -> isAsciiLower c || isDigit c) <*> A.string ".onion"),
|
||||
THDomainName . B.unpack <$> (notOnion <$?> A.takeWhile1 (A.notInClass ":#,;/ \n\r\t"))
|
||||
]
|
||||
@@ -87,6 +91,9 @@ instance StrEncoding TransportHost where
|
||||
ipNum = validIP <$?> (A.decimal <* A.char '.')
|
||||
validIP :: Int -> Either String Word8
|
||||
validIP n = if 0 <= n && n <= 255 then Right $ fromIntegral n else Left "invalid IP address"
|
||||
ipv6StrP =
|
||||
A.char '[' *> A.takeWhile1 (/= ']') <* A.char ']'
|
||||
<|> A.takeWhile1 (\c -> isHexDigit c || c == ':')
|
||||
notOnion s = if ".onion" `B.isSuffixOf` s then Left "invalid onion host" else Right s
|
||||
|
||||
instance ToJSON TransportHost where
|
||||
@@ -118,7 +125,8 @@ data TransportClientConfig = TransportClientConfig
|
||||
tcpKeepAlive :: Maybe KeepAliveOpts,
|
||||
logTLSErrors :: Bool,
|
||||
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey),
|
||||
alpn :: Maybe [ALPN]
|
||||
alpn :: Maybe [ALPN],
|
||||
useSNI :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -127,23 +135,23 @@ defaultTcpConnectTimeout :: Int
|
||||
defaultTcpConnectTimeout = 25_000_000
|
||||
|
||||
defaultTransportClientConfig :: TransportClientConfig
|
||||
defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing
|
||||
defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing True
|
||||
|
||||
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 => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
|
||||
runTransportClient = runTLSTransportClient supportedParameters Nothing
|
||||
runTransportClient :: Transport c => TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
|
||||
runTransportClient = runTLSTransportClient defaultSupportedParams Nothing
|
||||
|
||||
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
|
||||
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
|
||||
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn, useSNI} socksCreds host port keyHash client = do
|
||||
serverCert <- newEmptyTMVarIO
|
||||
let hostName = B.unpack $ strEncode host
|
||||
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert
|
||||
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn useSNI serverCert
|
||||
connectTCP = case socksProxy of
|
||||
Just proxy -> connectSocksClient proxy proxyUsername (hostAddr host)
|
||||
Just proxy -> connectSocksClient proxy socksCreds (hostAddr host)
|
||||
_ -> connectTCPClient hostName
|
||||
c <- do
|
||||
sock <- connectTCP port
|
||||
@@ -191,45 +199,76 @@ connectTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
|
||||
defaultSMPPort :: PortNumber
|
||||
defaultSMPPort = 5223
|
||||
|
||||
connectSocksClient :: SocksProxy -> Maybe ByteString -> SocksHostAddress -> ServiceName -> IO Socket
|
||||
connectSocksClient (SocksProxy addr) proxyUsername hostAddr _port = do
|
||||
connectSocksClient :: SocksProxy -> Maybe SocksCredentials -> SocksHostAddress -> ServiceName -> IO Socket
|
||||
connectSocksClient (SocksProxy addr) socksCreds hostAddr _port = do
|
||||
let port = if null _port then defaultSMPPort else fromMaybe defaultSMPPort $ readMaybe _port
|
||||
fst <$> case proxyUsername of
|
||||
Just username -> socksConnectAuth (defaultSocksConf addr) (SocksAddress hostAddr port) (SocksCredentials username "")
|
||||
fst <$> case socksCreds of
|
||||
Just creds -> socksConnectAuth (defaultSocksConf addr) (SocksAddress hostAddr port) creds
|
||||
_ -> socksConnect (defaultSocksConf addr) (SocksAddress hostAddr port)
|
||||
|
||||
defaultSocksHost :: HostAddress
|
||||
defaultSocksHost = tupleToHostAddress (127, 0, 0, 1)
|
||||
defaultSocksHost :: (Word8, Word8, Word8, Word8)
|
||||
defaultSocksHost = (127, 0, 0, 1)
|
||||
|
||||
defaultSocksProxyWithAuth :: SocksProxyWithAuth
|
||||
defaultSocksProxyWithAuth = SocksProxyWithAuth SocksIsolateByAuth defaultSocksProxy
|
||||
|
||||
defaultSocksProxy :: SocksProxy
|
||||
defaultSocksProxy = SocksProxy $ SockAddrInet 9050 defaultSocksHost
|
||||
defaultSocksProxy = SocksProxy $ SockAddrInet 9050 $ tupleToHostAddress defaultSocksHost
|
||||
|
||||
newtype SocksProxy = SocksProxy SockAddr
|
||||
deriving (Eq)
|
||||
|
||||
data SocksProxyWithAuth = SocksProxyWithAuth SocksAuth SocksProxy
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SocksAuth
|
||||
= SocksAuthUsername {username :: ByteString, password :: ByteString}
|
||||
| SocksAuthNull
|
||||
| SocksIsolateByAuth -- this is default
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Show SocksProxy where show (SocksProxy addr) = show addr
|
||||
|
||||
instance StrEncoding SocksProxy where
|
||||
strEncode = B.pack . show
|
||||
strP = do
|
||||
host <- maybe defaultSocksHost tupleToHostAddress <$> optional ipv4P
|
||||
host <- fromMaybe (THIPv4 defaultSocksHost) <$> optional strP
|
||||
port <- fromMaybe 9050 <$> optional (A.char ':' *> (fromInteger <$> A.decimal))
|
||||
pure . SocksProxy $ SockAddrInet port host
|
||||
SocksProxy <$> socksAddr port host
|
||||
where
|
||||
ipv4P = (,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal
|
||||
ipNum = A.decimal <* A.char '.'
|
||||
socksAddr port = \case
|
||||
THIPv4 addr -> pure $ SockAddrInet port $ tupleToHostAddress addr
|
||||
THIPv6 addr -> pure $ SockAddrInet6 port 0 addr 0
|
||||
_ -> fail "SOCKS5 host should be IPv4 or IPv6 address"
|
||||
|
||||
instance ToJSON SocksProxy where
|
||||
instance StrEncoding SocksProxyWithAuth where
|
||||
strEncode (SocksProxyWithAuth auth proxy) = strEncode auth <> strEncode proxy
|
||||
strP = SocksProxyWithAuth <$> strP <*> strP
|
||||
|
||||
instance ToJSON SocksProxyWithAuth where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromJSON SocksProxy where
|
||||
parseJSON = strParseJSON "SocksProxy"
|
||||
instance FromJSON SocksProxyWithAuth where
|
||||
parseJSON = strParseJSON "SocksProxyWithAuth"
|
||||
|
||||
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 =
|
||||
instance StrEncoding SocksAuth where
|
||||
strEncode = \case
|
||||
SocksAuthUsername {username, password} -> username <> ":" <> password <> "@"
|
||||
SocksAuthNull -> "@"
|
||||
SocksIsolateByAuth -> ""
|
||||
strP = usernameP <|> (SocksAuthNull <$ A.char '@') <|> pure SocksIsolateByAuth
|
||||
where
|
||||
usernameP = do
|
||||
username <- A.takeTill (== ':') <* A.char ':'
|
||||
password <- A.takeTill (== '@') <* A.char '@'
|
||||
pure SocksAuthUsername {username, password}
|
||||
|
||||
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> Bool -> TMVar X.CertificateChain -> T.ClientParams
|
||||
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ sni serverCerts =
|
||||
(T.defaultParamsClient host p)
|
||||
{ T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
|
||||
{ T.clientUseServerNameIndication = sni,
|
||||
T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
|
||||
T.clientHooks =
|
||||
def
|
||||
{ T.onServerCertificate = onServerCert,
|
||||
|
||||
@@ -32,8 +32,8 @@ import qualified Time.System as Hourglass
|
||||
-- leaf <- genCredentials (Just ca) (0, 1) "Entity" -- session-signing cert
|
||||
-- pure $ tlsCredentials (leaf :| [ca])
|
||||
-- @
|
||||
tlsCredentials :: NonEmpty Credentials -> (C.KeyHash, TLS.Credentials)
|
||||
tlsCredentials credentials = (C.KeyHash rootFP, TLS.Credentials [(X509.CertificateChain certs, privateToTls $ snd leafKey)])
|
||||
tlsCredentials :: NonEmpty Credentials -> (C.KeyHash, TLS.Credential)
|
||||
tlsCredentials credentials = (C.KeyHash rootFP, (X509.CertificateChain certs, privateToTls $ snd leafKey))
|
||||
where
|
||||
Fingerprint rootFP = getFingerprint root X509.HashSHA256
|
||||
leafKey = fst $ L.head credentials
|
||||
|
||||
@@ -11,7 +11,6 @@ import Control.Concurrent.Async
|
||||
import Control.Exception (IOException, try)
|
||||
import qualified Control.Exception as E
|
||||
import Control.Monad
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Functor (($>))
|
||||
import Data.Time (UTCTime, getCurrentTime)
|
||||
import qualified Data.X509 as X
|
||||
@@ -20,6 +19,7 @@ import Network.HPACK (BufferSize)
|
||||
import Network.HTTP2.Client (ClientConfig (..), Request, Response)
|
||||
import qualified Network.HTTP2.Client as H
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import Network.Socks5 (SocksCredentials)
|
||||
import qualified Network.TLS as T
|
||||
import Numeric.Natural (Natural)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -78,7 +78,8 @@ defaultHTTP2ClientConfig =
|
||||
tcpKeepAlive = Nothing,
|
||||
logTLSErrors = True,
|
||||
clientCredentials = Nothing,
|
||||
alpn = Nothing
|
||||
alpn = Nothing,
|
||||
useSNI = True
|
||||
},
|
||||
bufferSize = defaultHTTP2BufferSize,
|
||||
bodyHeadSize = 16384,
|
||||
@@ -91,10 +92,10 @@ data HTTP2ClientError = HCResponseTimeout | HCNetworkError | HCIOError IOExcepti
|
||||
getHTTP2Client :: HostName -> ServiceName -> Maybe XS.CertificateStore -> HTTP2ClientConfig -> IO () -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getHTTP2Client host port = getVerifiedHTTP2Client Nothing (THDomainName host) port Nothing
|
||||
|
||||
getVerifiedHTTP2Client :: Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> Maybe XS.CertificateStore -> HTTP2ClientConfig -> IO () -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getVerifiedHTTP2Client proxyUsername host port keyHash caStore config disconnected = getVerifiedHTTP2ClientWith config host port disconnected setup
|
||||
getVerifiedHTTP2Client :: Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> Maybe XS.CertificateStore -> HTTP2ClientConfig -> IO () -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getVerifiedHTTP2Client socksCreds host port keyHash caStore config disconnected = getVerifiedHTTP2ClientWith config host port disconnected setup
|
||||
where
|
||||
setup = runHTTP2Client (suportedTLSParams config) caStore (transportConfig config) (bufferSize config) proxyUsername host port keyHash
|
||||
setup = runHTTP2Client (suportedTLSParams config) caStore (transportConfig config) (bufferSize config) socksCreds host port keyHash
|
||||
|
||||
attachHTTP2Client :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> Int -> TLS -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
attachHTTP2Client config host port disconnected bufferSize tls = getVerifiedHTTP2ClientWith config host port disconnected setup
|
||||
@@ -178,11 +179,11 @@ 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 -> (TLS -> H.Client a) -> IO a
|
||||
runHTTP2Client tlsParams caStore tcConfig bufferSize proxyUsername host port keyHash = runHTTP2ClientWith bufferSize host setup
|
||||
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (TLS -> H.Client a) -> IO a
|
||||
runHTTP2Client tlsParams caStore tcConfig bufferSize socksCreds host port keyHash = runHTTP2ClientWith bufferSize host setup
|
||||
where
|
||||
setup :: (TLS -> IO a) -> IO a
|
||||
setup = runTLSTransportClient tlsParams caStore tcConfig proxyUsername host port keyHash
|
||||
setup = runTLSTransportClient tlsParams caStore tcConfig socksCreds host port keyHash
|
||||
|
||||
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
|
||||
|
||||
@@ -15,7 +15,7 @@ import Numeric.Natural (Natural)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
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.Transport.Server (ServerCredentials, TransportServerConfig (..), loadServerCredential, runTransportServer)
|
||||
import Simplex.Messaging.Util (threadDelay')
|
||||
import UnliftIO (finally)
|
||||
import UnliftIO.Concurrent (forkIO, killThread)
|
||||
@@ -28,9 +28,7 @@ data HTTP2ServerConfig = HTTP2ServerConfig
|
||||
bufferSize :: BufferSize,
|
||||
bodyHeadSize :: Int,
|
||||
serverSupported :: T.Supported,
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
https2Credentials :: ServerCredentials,
|
||||
transportConfig :: TransportServerConfig
|
||||
}
|
||||
deriving (Show)
|
||||
@@ -49,13 +47,13 @@ data HTTP2Server = HTTP2Server
|
||||
}
|
||||
|
||||
-- This server is for testing only, it processes all requests in a single queue.
|
||||
getHTTP2Server :: HTTP2ServerConfig -> IO HTTP2Server
|
||||
getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, serverSupported, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
|
||||
tlsServerParams <- loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
getHTTP2Server :: HTTP2ServerConfig -> Maybe [ALPN] -> IO HTTP2Server
|
||||
getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, serverSupported, https2Credentials, transportConfig} alpn_ = do
|
||||
srvCreds <- loadServerCredential https2Credentials
|
||||
started <- newEmptyTMVarIO
|
||||
reqQ <- newTBQueueIO qSize
|
||||
action <- async $
|
||||
runHTTP2Server started http2Port bufferSize tlsServerParams transportConfig Nothing (const $ pure ()) $ \sessionId sessionALPN r sendResponse -> do
|
||||
runHTTP2Server started http2Port bufferSize serverSupported srvCreds alpn_ transportConfig Nothing (const $ pure ()) $ \sessionId sessionALPN r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r bodyHeadSize
|
||||
atomically $ writeTBQueue reqQ HTTP2Request {sessionId, sessionALPN, request = r, reqBody, sendResponse}
|
||||
void . atomically $ takeTMVar started
|
||||
@@ -64,10 +62,10 @@ getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, se
|
||||
closeHTTP2Server :: HTTP2Server -> IO ()
|
||||
closeHTTP2Server = uninterruptibleCancel . action
|
||||
|
||||
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
|
||||
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> Maybe ExpirationConfig -> (SessionId -> IO ()) -> HTTP2ServerFunc -> IO ()
|
||||
runHTTP2Server started port bufferSize srvSupported srvCreds alpn_ transportConfig expCfg_ clientFinished = runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup
|
||||
where
|
||||
setup = runTransportServer started port serverParams transportConfig
|
||||
setup = runTransportServer started port srvSupported srvCreds alpn_ transportConfig
|
||||
|
||||
runHTTP2ServerWith :: BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
|
||||
runHTTP2ServerWith = runHTTP2ServerWith_ Nothing (\_sessId -> pure ())
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
|
||||
module Simplex.Messaging.Transport.Server
|
||||
( TransportServerConfig (..),
|
||||
ServerCredentials (..),
|
||||
AddHTTP,
|
||||
defaultTransportServerConfig,
|
||||
runTransportServerState,
|
||||
runTransportServerState_,
|
||||
SocketState,
|
||||
newSocketState,
|
||||
runTransportServer,
|
||||
@@ -15,11 +18,12 @@ module Simplex.Messaging.Transport.Server
|
||||
runLocalTCPServer,
|
||||
runTCPServerSocket,
|
||||
startTCPServer,
|
||||
loadSupportedTLSServerParams,
|
||||
loadTLSServerParams,
|
||||
loadServerCredential,
|
||||
supportedTLSServerParams,
|
||||
supportedTLSServerParams_,
|
||||
loadFingerprint,
|
||||
loadFileFingerprint,
|
||||
smpServerHandshake,
|
||||
tlsServerCredentials,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -31,7 +35,7 @@ import Data.Default (def)
|
||||
import Data.IntMap.Strict (IntMap)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import Data.List (find)
|
||||
import Data.Maybe (fromJust, fromMaybe)
|
||||
import Data.Maybe (fromJust, fromMaybe, maybeToList)
|
||||
import qualified Data.X509 as X
|
||||
import Data.X509.Validation (Fingerprint (..))
|
||||
import qualified Data.X509.Validation as XV
|
||||
@@ -52,18 +56,25 @@ import UnliftIO.STM
|
||||
data TransportServerConfig = TransportServerConfig
|
||||
{ logTLSErrors :: Bool,
|
||||
tlsSetupTimeout :: Int,
|
||||
transportTimeout :: Int,
|
||||
alpn :: Maybe [ALPN]
|
||||
transportTimeout :: Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ServerCredentials = ServerCredentials
|
||||
{ caCertificateFile :: Maybe FilePath, -- CA certificate private key is not needed for initialization
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
type AddHTTP = Bool
|
||||
|
||||
defaultTransportServerConfig :: TransportServerConfig
|
||||
defaultTransportServerConfig =
|
||||
TransportServerConfig
|
||||
{ logTLSErrors = True,
|
||||
tlsSetupTimeout = 60000000,
|
||||
transportTimeout = 40000000,
|
||||
alpn = Nothing
|
||||
transportTimeout = 40000000
|
||||
}
|
||||
|
||||
serverTransportConfig :: TransportServerConfig -> TransportConfig
|
||||
@@ -74,37 +85,41 @@ 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. Transport c => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServer started port params cfg server = do
|
||||
runTransportServer :: forall c. Transport c => TMVar Bool -> ServiceName -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServer started port srvSupported srvCreds alpn_ cfg server = do
|
||||
ss <- newSocketState
|
||||
runTransportServerState ss started port params cfg server
|
||||
runTransportServerState ss started port srvSupported srvCreds alpn_ cfg server
|
||||
|
||||
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started Nothing port) (transportName (TProxy :: TProxy c))
|
||||
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServerState ss started port srvSupported srvCreds alpn_ cfg server = runTransportServerState_ ss started port srvSupported (const srvCreds) alpn_ cfg (const server)
|
||||
|
||||
runTransportServerState_ :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> TransportServerConfig -> (Socket -> c -> IO ()) -> IO ()
|
||||
runTransportServerState_ ss started port = runTransportServerSocketState ss started (startTCPServer started Nothing port) (transportName (TProxy :: TProxy c))
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
|
||||
runTransportServerSocket started getSocket threadLabel serverParams cfg server = do
|
||||
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.Credential -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
|
||||
runTransportServerSocket started getSocket threadLabel srvCreds srvParams cfg server = do
|
||||
ss <- newSocketState
|
||||
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server
|
||||
runTransportServerSocketState_ ss started getSocket threadLabel (const srvCreds) srvParams cfg (const server)
|
||||
|
||||
runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> TransportServerConfig -> (Socket -> a -> IO ()) -> IO ()
|
||||
runTransportServerSocketState ss started getSocket threadLabel srvSupported srvCreds alpn_ =
|
||||
runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams
|
||||
where
|
||||
srvParams = supportedTLSServerParams_ srvSupported srvCreds alpn_
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
|
||||
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server = do
|
||||
runTransportServerSocketState_ :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> (Maybe HostName -> (X.CertificateChain, X.PrivKey)) -> T.ServerParams -> TransportServerConfig -> (Socket -> a -> IO ()) -> IO ()
|
||||
runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams cfg server = do
|
||||
labelMyThread $ "transport server for " <> threadLabel
|
||||
runTCPServerSocket ss started getSocket $ \conn ->
|
||||
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection server
|
||||
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection (server conn)
|
||||
where
|
||||
tCfg = serverTransportConfig cfg
|
||||
setup conn = timeout (tlsSetupTimeout cfg) $ do
|
||||
labelMyThread $ threadLabel <> "/setup"
|
||||
tls <- connectTLS Nothing tCfg serverParams conn
|
||||
getServerConnection tCfg (fst $ tlsServerCredentials serverParams) tls
|
||||
|
||||
tlsServerCredentials :: T.ServerParams -> (X.CertificateChain, X.PrivKey)
|
||||
tlsServerCredentials serverParams = case T.sharedCredentials $ T.serverShared serverParams of
|
||||
T.Credentials [creds] -> creds
|
||||
_ -> error "server has more than one key"
|
||||
tls <- connectTLS Nothing tCfg srvParams conn
|
||||
getServerConnection tCfg (fst $ srvCreds Nothing) tls
|
||||
|
||||
-- | Run TCP server without TLS
|
||||
runLocalTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO ()
|
||||
@@ -176,30 +191,33 @@ startTCPServer started host port = withSocketsDo $ resolve >>= open >>= setStart
|
||||
pure sock
|
||||
setStarted sock = atomically (tryPutTMVar started True) >> pure sock
|
||||
|
||||
loadTLSServerParams :: FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams
|
||||
loadTLSServerParams = loadSupportedTLSServerParams supportedParameters
|
||||
loadServerCredential :: ServerCredentials -> IO T.Credential
|
||||
loadServerCredential ServerCredentials {caCertificateFile, certificateFile, privateKeyFile} =
|
||||
T.credentialLoadX509Chain certificateFile (maybeToList caCertificateFile) privateKeyFile >>= \case
|
||||
Right credential -> pure credential
|
||||
Left _ -> putStrLn "invalid credential" >> exitFailure
|
||||
|
||||
loadSupportedTLSServerParams :: T.Supported -> FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams
|
||||
loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile alpn_ = do
|
||||
tlsServerParams <- fromCredential <$> loadServerCredential
|
||||
pure tlsServerParams {T.serverHooks = maybe def alpnHooks alpn_}
|
||||
where
|
||||
loadServerCredential :: IO T.Credential
|
||||
loadServerCredential =
|
||||
T.credentialLoadX509Chain certificateFile [caCertificateFile] privateKeyFile >>= \case
|
||||
Right credential -> pure credential
|
||||
Left _ -> putStrLn "invalid credential" >> exitFailure
|
||||
fromCredential :: T.Credential -> T.ServerParams
|
||||
fromCredential credential =
|
||||
def
|
||||
{ T.serverWantClientCert = False,
|
||||
T.serverShared = def {T.sharedCredentials = T.Credentials [credential]},
|
||||
T.serverHooks = def,
|
||||
T.serverSupported = serverSupported
|
||||
}
|
||||
alpnHooks supported = def {T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported)}
|
||||
supportedTLSServerParams :: T.Credential -> Maybe [ALPN] -> T.ServerParams
|
||||
supportedTLSServerParams = supportedTLSServerParams_ defaultSupportedParams . const
|
||||
|
||||
loadFingerprint :: FilePath -> IO Fingerprint
|
||||
loadFingerprint certificateFile = do
|
||||
supportedTLSServerParams_ :: T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> T.ServerParams
|
||||
supportedTLSServerParams_ serverSupported creds alpn_ =
|
||||
def
|
||||
{ T.serverWantClientCert = False,
|
||||
T.serverHooks =
|
||||
def
|
||||
{ T.onServerNameIndication = \host_ -> pure $ T.Credentials [creds host_],
|
||||
T.onALPNClientSuggest = (\alpn -> pure . fromMaybe "" . find (`elem` alpn)) <$> alpn_
|
||||
},
|
||||
T.serverSupported = serverSupported
|
||||
}
|
||||
|
||||
loadFingerprint :: ServerCredentials -> IO Fingerprint
|
||||
loadFingerprint ServerCredentials {caCertificateFile} = case caCertificateFile of
|
||||
Just certificateFile -> loadFileFingerprint certificateFile
|
||||
Nothing -> error "CA file must be used in protocol credentials"
|
||||
|
||||
loadFileFingerprint :: FilePath -> IO Fingerprint
|
||||
loadFileFingerprint certificateFile = do
|
||||
(cert : _) <- SX.readSignedObject certificateFile
|
||||
pure $ XV.getFingerprint (cert :: X.SignedExact X.Certificate) X.HashSHA256
|
||||
|
||||
@@ -23,6 +23,9 @@ module Simplex.RemoteControl.Client
|
||||
rcEncryptBody,
|
||||
rcDecryptBody,
|
||||
xrcpBlockSize,
|
||||
-- for tests only
|
||||
sendRCPacket,
|
||||
receiveRCPacket,
|
||||
) where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
@@ -33,6 +36,7 @@ import Control.Monad.IO.Class
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson as J
|
||||
import Data.Bitraversable (bimapM)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
@@ -43,6 +47,7 @@ import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (isNothing)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock.System (getSystemTime)
|
||||
import Data.Tuple (swap)
|
||||
import Data.Word (Word16)
|
||||
import qualified Data.X509 as X509
|
||||
import Data.X509.Validation (Fingerprint (..), getFingerprint)
|
||||
@@ -57,7 +62,7 @@ import Simplex.Messaging.Crypto.SNTRUP761
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Transport (TLS (..), cGet, cPut)
|
||||
import Simplex.Messaging.Transport (TSbChainKeys (..), TLS (..), cGet, cPut)
|
||||
import Simplex.Messaging.Transport.Buffer (peekBuffered)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), defaultTransportClientConfig, runTransportClient)
|
||||
import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials)
|
||||
@@ -185,7 +190,7 @@ connectRCHost drg pairing@RCHostPairing {caKey, caCert, idPrivKey, knownHost} ct
|
||||
}
|
||||
pure $ signInvitation (snd sessKeys) idPrivKey inv
|
||||
|
||||
genTLSCredentials :: TVar ChaChaDRG -> C.APrivateSignKey -> C.SignedCertificate -> IO TLS.Credentials
|
||||
genTLSCredentials :: TVar ChaChaDRG -> C.APrivateSignKey -> C.SignedCertificate -> IO TLS.Credential
|
||||
genTLSCredentials drg caKey caCert = do
|
||||
let caCreds = (C.signatureKeyPair caKey, caCert)
|
||||
leaf <- genCredentials drg (Just caCreds) (0, 24 * 999999) "localhost" -- session-signing cert
|
||||
@@ -214,15 +219,16 @@ prepareHostSession
|
||||
hostHello@RCHostHello {v, ca, kem = kemPubKey} <- liftEitherWith RCESyntax $ J.eitherDecodeStrict helloBody
|
||||
unless (ca == tlsHostFingerprint) $ throwE RCEIdentity
|
||||
(kemCiphertext, kemSharedKey) <- liftIO $ sntrup761Enc drg kemPubKey
|
||||
let hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey
|
||||
let KEMHybridSecret hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey
|
||||
unless (isCompatible v supportedRCPVRange) $ throwE RCEVersion
|
||||
let keys = HostSessKeys {hybridKey, idPrivKey, sessPrivKey}
|
||||
(sndKey, rcvKey) <- bimapM newTVarIO newTVarIO $ C.sbcInit "" hybridKey
|
||||
let keys = HostSessKeys {chainKeys = TSbChainKeys {sndKey, rcvKey}, idPrivKey, sessPrivKey}
|
||||
knownHost' <- updateKnownHost ca dhPubKey
|
||||
let ctrlHello = RCCtrlHello {}
|
||||
-- TODO send error response if something fails
|
||||
nonce' <- liftIO . atomically $ C.randomCbNonce drg
|
||||
encBody' <- liftEitherWith (const RCEBlockSize) $ kcbEncrypt hybridKey nonce' (LB.toStrict $ J.encode ctrlHello) helloBlockSize
|
||||
let ctrlEncHello = RCCtrlEncHello {kem = kemCiphertext, nonce = nonce', encBody = encBody'}
|
||||
(sk, nonce') <- atomically $ stateTVar sndKey C.sbcHkdf
|
||||
encBody' <- liftEitherWith (const RCEBlockSize) $ C.sbEncrypt sk nonce' (LB.toStrict $ J.encode ctrlHello) helloBlockSize
|
||||
let ctrlEncHello = RCCtrlEncHello {kem = kemCiphertext, encBody = encBody'}
|
||||
pure (ctrlEncHello, keys, hostHello, pairing {knownHost = Just knownHost'})
|
||||
where
|
||||
updateKnownHost :: C.KeyHash -> C.PublicKeyX25519 -> ExceptT RCErrorType IO KnownHostPairing
|
||||
@@ -276,10 +282,7 @@ connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca,
|
||||
pure RCCClient_ {confirmSession, endSession}
|
||||
runClient :: RCCClient_ -> RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> ExceptT RCErrorType IO ()
|
||||
runClient RCCClient_ {confirmSession, endSession} r = do
|
||||
clientCredentials <-
|
||||
liftIO (genTLSCredentials drg caKey caCert) >>= \case
|
||||
TLS.Credentials (creds : _) -> pure $ Just creds
|
||||
_ -> throwE $ RCEInternal "genTLSCredentials must generate credentials"
|
||||
clientCredentials <- liftIO $ Just <$> genTLSCredentials drg caKey caCert
|
||||
let clientConfig = defaultTransportClientConfig {clientCredentials}
|
||||
ExceptT . runTransportClient clientConfig Nothing host (show port) (Just ca) $ \tls@TLS {tlsBuffer, tlsContext} -> runExceptT $ do
|
||||
-- pump socket to detect connection problems
|
||||
@@ -349,13 +352,16 @@ prepareCtrlSession
|
||||
RCInvitation {skey, dh = dhPubKey}
|
||||
sharedKey
|
||||
kemPrivKey = \case
|
||||
RCCtrlEncHello {kem = kemCiphertext, nonce, encBody} -> do
|
||||
RCCtrlEncHello {kem = kemCiphertext, encBody} -> do
|
||||
kemSharedKey <- liftIO $ sntrup761Dec kemCiphertext kemPrivKey
|
||||
let hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey
|
||||
helloBody <- liftEitherWith (const RCEDecrypt) $ kcbDecrypt hybridKey nonce encBody
|
||||
let KEMHybridSecret hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey
|
||||
-- keys are swapped in controller
|
||||
(sndKey, rcvKey) <- swap <$> bimapM newTVarIO newTVarIO (C.sbcInit "" hybridKey)
|
||||
(sk, nonce) <- atomically $ stateTVar rcvKey C.sbcHkdf
|
||||
helloBody <- liftEitherWith (const RCEDecrypt) $ C.sbDecrypt sk nonce encBody
|
||||
logDebug "Decrypted ctrl HELLO"
|
||||
RCCtrlHello {} <- liftEitherWith RCESyntax $ J.eitherDecodeStrict helloBody
|
||||
pure CtrlSessKeys {hybridKey, idPubKey, sessPubKey = skey}
|
||||
pure CtrlSessKeys {chainKeys = TSbChainKeys {sndKey, rcvKey}, idPubKey, sessPubKey = skey}
|
||||
RCCtrlEncError {nonce, encMessage} -> do
|
||||
message <- liftEitherWith (const RCEDecrypt) $ C.cbDecrypt sharedKey nonce encMessage
|
||||
throwE $ RCECtrlError $ T.unpack $ safeDecodeUtf8 message
|
||||
@@ -426,17 +432,14 @@ cancelCtrlClient RCCtrlClient {action, client_ = RCCClient_ {endSession}} = do
|
||||
|
||||
-- * Session encryption
|
||||
|
||||
rcEncryptBody :: TVar ChaChaDRG -> KEMHybridSecret -> LazyByteString -> ExceptT RCErrorType IO (C.CbNonce, LazyByteString)
|
||||
rcEncryptBody drg hybridKey s = do
|
||||
nonce <- atomically $ C.randomCbNonce drg
|
||||
let len = LB.length s
|
||||
ct <- liftEitherWith (const RCEEncrypt) $ LC.kcbEncryptTailTag hybridKey nonce s len (len + 8)
|
||||
pure (nonce, ct)
|
||||
rcEncryptBody :: C.SbKeyNonce -> LazyByteString -> ExceptT RCErrorType IO LazyByteString
|
||||
rcEncryptBody keyNonce s = do
|
||||
liftEitherWith (const RCEEncrypt) $ LC.sbEncryptTailTagNoPad keyNonce s
|
||||
|
||||
rcDecryptBody :: KEMHybridSecret -> C.CbNonce -> LazyByteString -> ExceptT RCErrorType IO LazyByteString
|
||||
rcDecryptBody hybridKey nonce ct = do
|
||||
rcDecryptBody :: C.SbKeyNonce -> LazyByteString -> ExceptT RCErrorType IO LazyByteString
|
||||
rcDecryptBody keyNonce ct = do
|
||||
let len = LB.length ct - 16
|
||||
when (len < 0) $ throwE RCEDecrypt
|
||||
(ok, s) <- liftEitherWith (const RCEDecrypt) $ LC.kcbDecryptTailTag hybridKey nonce len ct
|
||||
(ok, s) <- liftEitherWith (const RCEDecrypt) $ LC.sbDecryptTailTagNoPad keyNonce len ct
|
||||
unless ok $ throwE RCEDecrypt
|
||||
pure s
|
||||
|
||||
@@ -23,7 +23,7 @@ import Network.Info (IPv4 (..), NetworkInterface (..), getNetworkInterfaces)
|
||||
import qualified Network.Socket as N
|
||||
import qualified Network.TLS as TLS
|
||||
import qualified Network.UDP as UDP
|
||||
import Simplex.Messaging.Transport (supportedParameters)
|
||||
import Simplex.Messaging.Transport (defaultSupportedParams)
|
||||
import qualified Simplex.Messaging.Transport as Transport
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (defaultTransportServerConfig, runTransportServerSocket, startTCPServer)
|
||||
@@ -68,7 +68,7 @@ preferAddress RCCtrlAddress {address, interface} addrs =
|
||||
matchAddr RCCtrlAddress {address = a} = a == address
|
||||
matchIface RCCtrlAddress {interface = i} = i == interface
|
||||
|
||||
startTLSServer :: Maybe Word16 -> TMVar (Maybe N.PortNumber) -> TLS.Credentials -> TLS.ServerHooks -> (Transport.TLS -> IO ()) -> IO (Async ())
|
||||
startTLSServer :: Maybe Word16 -> TMVar (Maybe N.PortNumber) -> TLS.Credential -> TLS.ServerHooks -> (Transport.TLS -> IO ()) -> IO (Async ())
|
||||
startTLSServer port_ startedOnPort credentials hooks server = async . liftIO $ do
|
||||
started <- newEmptyTMVarIO
|
||||
bracketOnError (startTCPServer started Nothing $ maybe "0" show port_) (\_e -> setPort Nothing) $ \socket ->
|
||||
@@ -81,14 +81,14 @@ startTLSServer port_ startedOnPort credentials hooks server = async . liftIO $ d
|
||||
port <- N.socketPort socket
|
||||
logInfo $ "System-assigned port: " <> tshow port
|
||||
setPort $ Just port
|
||||
runTransportServerSocket started (pure socket) "RCP TLS" serverParams defaultTransportServerConfig server
|
||||
runTransportServerSocket started (pure socket) "RCP TLS" credentials serverParams defaultTransportServerConfig server
|
||||
setPort = void . atomically . tryPutTMVar startedOnPort
|
||||
serverParams =
|
||||
def
|
||||
{ TLS.serverWantClientCert = True,
|
||||
TLS.serverShared = def {TLS.sharedCredentials = credentials},
|
||||
TLS.serverShared = def {TLS.sharedCredentials = TLS.Credentials [credentials]},
|
||||
TLS.serverHooks = hooks,
|
||||
TLS.serverSupported = supportedParameters
|
||||
TLS.serverSupported = defaultSupportedParams
|
||||
}
|
||||
|
||||
withSender :: (UDP.UDPSocket -> IO a) -> IO a
|
||||
|
||||
@@ -20,12 +20,11 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word16)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.SNTRUP761
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport (TLS)
|
||||
import Simplex.Messaging.Transport (TLS, TSbChainKeys)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Version (VersionRange, VersionScope, mkVersionRange)
|
||||
@@ -180,7 +179,7 @@ data RCHostSession = RCHostSession
|
||||
}
|
||||
|
||||
data HostSessKeys = HostSessKeys
|
||||
{ hybridKey :: KEMHybridSecret,
|
||||
{ chainKeys :: TSbChainKeys,
|
||||
idPrivKey :: C.PrivateKeyEd25519,
|
||||
sessPrivKey :: C.PrivateKeyEd25519
|
||||
}
|
||||
@@ -193,7 +192,7 @@ data RCCtrlSession = RCCtrlSession
|
||||
}
|
||||
|
||||
data CtrlSessKeys = CtrlSessKeys
|
||||
{ hybridKey :: KEMHybridSecret,
|
||||
{ chainKeys :: TSbChainKeys,
|
||||
idPubKey :: C.PublicKeyEd25519,
|
||||
sessPubKey :: C.PublicKeyEd25519
|
||||
}
|
||||
@@ -213,19 +212,19 @@ instance Encoding RCHostEncHello where
|
||||
pure RCHostEncHello {dhPubKey, nonce, encBody}
|
||||
|
||||
data RCCtrlEncHello
|
||||
= RCCtrlEncHello {kem :: KEMCiphertext, nonce :: C.CbNonce, encBody :: ByteString}
|
||||
= RCCtrlEncHello {kem :: KEMCiphertext, encBody :: ByteString}
|
||||
| RCCtrlEncError {nonce :: C.CbNonce, encMessage :: ByteString}
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding RCCtrlEncHello where
|
||||
smpEncode = \case
|
||||
RCCtrlEncHello {kem, nonce, encBody} -> "HELLO " <> smpEncode (kem, nonce, Tail encBody)
|
||||
RCCtrlEncHello {kem, encBody} -> "HELLO " <> smpEncode (kem, Tail encBody)
|
||||
RCCtrlEncError {nonce, encMessage} -> "ERROR " <> smpEncode (nonce, Tail encMessage)
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"HELLO" -> do
|
||||
(kem, nonce, Tail encBody) <- _smpP
|
||||
pure RCCtrlEncHello {kem, nonce, encBody}
|
||||
(kem, Tail encBody) <- _smpP
|
||||
pure RCCtrlEncHello {kem, encBody}
|
||||
"ERROR" -> do
|
||||
(nonce, Tail encMessage) <- _smpP
|
||||
pure RCCtrlEncError {nonce, encMessage}
|
||||
|
||||
@@ -85,7 +85,7 @@ import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT)
|
||||
import qualified Simplex.Messaging.Agent.Protocol as A
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew))
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction')
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (..), defaultClientConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
@@ -2869,7 +2869,7 @@ testCreateQueueAuth srvVersion clnt1 clnt2 sqSecured baseId = do
|
||||
getClient clientId (clntAuth, clntVersion) db =
|
||||
let servers = initAgentServers {smp = userServers' [ProtoServerWithAuth testSMPServer clntAuth]}
|
||||
alpn_ = if clntVersion >= authCmdsSMPVersion then Just supportedSMPHandshakes else Nothing
|
||||
smpCfg = defaultClientConfig alpn_ $ V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion
|
||||
smpCfg = defaultClientConfig alpn_ False $ V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion
|
||||
sndAuthAlg = if srvVersion >= authCmdsSMPVersion && clntVersion >= authCmdsSMPVersion then C.AuthAlg C.SX25519 else C.AuthAlg C.SEd25519
|
||||
in getSMPAgentClient' clientId agentCfg {smpCfg, sndAuthAlg} servers db
|
||||
|
||||
@@ -3014,7 +3014,7 @@ testDeliveryReceiptsConcurrent t =
|
||||
testTwoUsers :: HasCallStack => IO ()
|
||||
testTwoUsers = withAgentClients2 $ \a b -> do
|
||||
let nc = netCfg initAgentServers
|
||||
sessionMode nc `shouldBe` TSMUser
|
||||
sessionMode nc `shouldBe` TSMSession
|
||||
runRight_ $ do
|
||||
(aId1, bId1) <- makeConnectionForUsers a 1 b 1
|
||||
exchangeGreetings a bId1 b aId1
|
||||
|
||||
@@ -166,7 +166,7 @@ testNtfMatrix t runTest = do
|
||||
it "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest
|
||||
it "curr servers; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
it "prev servers; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
it "prev servers; curr clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest
|
||||
it "prev servers; curr clients" $ runNtfTestCfg t 1 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest
|
||||
-- servers can be upgraded in any order
|
||||
it "servers: curr SMP, prev NTF; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
it "servers: prev SMP, curr NTF; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
@@ -178,7 +178,7 @@ runNtfTestCfg :: HasCallStack => ATransport -> AgentMsgId -> ServerConfig -> Ntf
|
||||
runNtfTestCfg t baseId smpCfg ntfCfg aCfg bCfg runTest = do
|
||||
withSmpServerConfigOn t smpCfg testPort $ \_ ->
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t)]} $ \_ ->
|
||||
withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t, False)]} $ \_ ->
|
||||
withAgentClientsCfg2 aCfg bCfg $ runTest apns baseId
|
||||
threadDelay 100000
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ storeTests = do
|
||||
it "should getNextDeletedSndChunkReplica" testGetNextDeletedSndChunkReplica
|
||||
it "should markNtfSubActionNtfFailed_" testMarkNtfSubActionNtfFailed
|
||||
it "should markNtfSubActionSMPFailed_" testMarkNtfSubActionSMPFailed
|
||||
it "should markNtfTokenToDeleteFailed_" testMarkNtfTokenToDeleteFailed
|
||||
describe "open/close store" $ do
|
||||
it "should close and re-open" testCloseReopenStore
|
||||
it "should close and re-open encrypted store" testCloseReopenEncryptedStore
|
||||
@@ -838,3 +839,8 @@ testMarkNtfSubActionSMPFailed :: SQLiteStore -> Expectation
|
||||
testMarkNtfSubActionSMPFailed st = do
|
||||
withTransaction st $ \db -> do
|
||||
markNtfSubActionSMPFailed_ db "abc"
|
||||
|
||||
testMarkNtfTokenToDeleteFailed :: SQLiteStore -> Expectation
|
||||
testMarkNtfTokenToDeleteFailed st = do
|
||||
withTransaction st $ \db -> do
|
||||
markNtfTokenToDeleteFailed_ db 1
|
||||
|
||||
+94
-6
@@ -3,15 +3,31 @@
|
||||
|
||||
module CLITests where
|
||||
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import AgentTests.FunctionalAPITests (runRight_)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import qualified Crypto.PubKey.RSA as RSA
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.Ini (Ini (..), lookupValue, readIniFile, writeIniFile)
|
||||
import Data.List (isPrefixOf)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.File as XF
|
||||
import Data.X509.Validation (Fingerprint (..))
|
||||
import qualified Network.HTTP.Client as H1
|
||||
import qualified Network.HTTP2.Client as H2
|
||||
import Simplex.FileTransfer.Server.Main (xftpServerCLI)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Server.Main
|
||||
import Simplex.Messaging.Server.Main
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Server.Main (smpServerCLI, smpServerCLI_)
|
||||
import Simplex.Messaging.Transport (TLS (..), defaultSupportedParams, defaultSupportedParamsHTTPS, simplexMQVersion, supportedClientSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), defaultTransportClientConfig, runTLSTransportClient, smpClientHandshake)
|
||||
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..))
|
||||
import qualified Simplex.Messaging.Transport.HTTP2.Client as HC
|
||||
import Simplex.Messaging.Transport.Server (loadFileFingerprint)
|
||||
import Simplex.Messaging.Util (catchAll_)
|
||||
import qualified Static
|
||||
import System.Directory (doesFileExist)
|
||||
import System.Environment (withArgs)
|
||||
import System.FilePath ((</>))
|
||||
@@ -19,6 +35,10 @@ import System.IO.Silently (capture_)
|
||||
import System.Timeout (timeout)
|
||||
import Test.Hspec
|
||||
import Test.Main (withStdin)
|
||||
import UnliftIO (catchAny)
|
||||
import UnliftIO.Async (async, cancel)
|
||||
import UnliftIO.Concurrent (threadDelay)
|
||||
import UnliftIO.Exception (bracket)
|
||||
|
||||
cfgPath :: FilePath
|
||||
cfgPath = "tests/tmp/cli/etc/opt/simplex"
|
||||
@@ -26,6 +46,9 @@ cfgPath = "tests/tmp/cli/etc/opt/simplex"
|
||||
logPath :: FilePath
|
||||
logPath = "tests/tmp/cli/etc/var/simplex"
|
||||
|
||||
webPath :: FilePath
|
||||
webPath = "tests/tmp/cli/var/www"
|
||||
|
||||
ntfCfgPath :: FilePath
|
||||
ntfCfgPath = "tests/tmp/cli/etc/opt/simplex-notifications"
|
||||
|
||||
@@ -46,6 +69,7 @@ cliTests = do
|
||||
it "with store log, random password (default)" $ smpServerTest True True
|
||||
it "no store log, no password" $ smpServerTest False False
|
||||
it "with store log, no password" $ smpServerTest True False
|
||||
it "static files" smpServerTestStatic
|
||||
describe "Ntf server CLI" $ do
|
||||
it "should initialize, start and delete the server (no store log)" $ ntfServerTest False
|
||||
it "should initialize, start and delete the server (with store log)" $ ntfServerTest True
|
||||
@@ -61,7 +85,7 @@ smpServerTest storeLog basicAuth = do
|
||||
Right ini <- readIniFile $ cfgPath <> "/smp-server.ini"
|
||||
lookupValue "STORE_LOG" "enable" ini `shouldBe` Right (if storeLog then "on" else "off")
|
||||
lookupValue "STORE_LOG" "log_stats" ini `shouldBe` Right "off"
|
||||
lookupValue "TRANSPORT" "port" ini `shouldBe` Right "5223"
|
||||
lookupValue "TRANSPORT" "port" ini `shouldBe` Right "5223,443"
|
||||
lookupValue "TRANSPORT" "websockets" ini `shouldBe` Right "off"
|
||||
lookupValue "AUTH" "new_queues" ini `shouldBe` Right "on"
|
||||
lookupValue "INACTIVE_CLIENTS" "disconnect" ini `shouldBe` Right "off"
|
||||
@@ -70,7 +94,7 @@ smpServerTest storeLog basicAuth = do
|
||||
r <- lines <$> capture_ (withArgs ["start"] $ (100000 `timeout` smpServerCLI cfgPath logPath) `catchAll_` pure (Just ()))
|
||||
r `shouldContain` ["SMP server v" <> simplexMQVersion]
|
||||
r `shouldContain` (if storeLog then ["Store log: " <> logPath <> "/smp-server-store.log"] else ["Store log disabled."])
|
||||
r `shouldContain` ["Listening on port 5223 (TLS)..."]
|
||||
r `shouldContain` ["Serving SMP protocol on port 5223 (TLS)...", "Serving SMP protocol on port 443 (TLS)...", "Serving static site on port 443 (TLS)..."]
|
||||
r `shouldContain` ["not expiring inactive clients"]
|
||||
r `shouldContain` (if basicAuth then ["creating new queues requires password"] else ["creating new queues allowed"])
|
||||
-- cert
|
||||
@@ -94,6 +118,70 @@ smpServerTest storeLog basicAuth = do
|
||||
>>= (`shouldSatisfy` ("WARNING: deleting the server will make all queues inaccessible" `isPrefixOf`))
|
||||
doesFileExist (cfgPath <> "/ca.key") `shouldReturn` False
|
||||
|
||||
smpServerTestStatic :: HasCallStack => IO ()
|
||||
smpServerTestStatic = do
|
||||
let iniFile = cfgPath <> "/smp-server.ini"
|
||||
capture_ (withArgs ["init", "-y", "--no-password", "--web-path", webPath] $ smpServerCLI cfgPath logPath)
|
||||
>>= (`shouldSatisfy` (("Server initialized, please provide additional server information in " <> iniFile) `isPrefixOf`))
|
||||
doesFileExist (cfgPath <> "/ca.key") `shouldReturn` True
|
||||
Right ini <- readIniFile iniFile
|
||||
lookupValue "WEB" "static_path" ini `shouldBe` Right (T.pack webPath)
|
||||
let transport = [("host", "localhost"), ("port", "5223"), ("log_tls_errors", "off"), ("websockets", "off")]
|
||||
web = [("http", "8000"), ("https", "5223"), ("cert", "tests/fixtures/web.crt"), ("key", "tests/fixtures/web.key"), ("static_path", T.pack webPath)]
|
||||
ini' = ini {iniSections = HM.insert "TRANSPORT" transport $ HM.insert "WEB" web (iniSections ini)}
|
||||
writeIniFile iniFile ini'
|
||||
|
||||
Right ini_ <- readIniFile iniFile
|
||||
lookupValue "WEB" "https" ini_ `shouldBe` Right "5223"
|
||||
|
||||
let smpServerCLI' = smpServerCLI_ Static.generateSite Static.serveStaticFiles Static.attachStaticFiles
|
||||
let server = capture_ (withArgs ["start"] $ smpServerCLI' cfgPath logPath `catchAny` print)
|
||||
bracket (async server) cancel $ \_t -> do
|
||||
threadDelay 1000000
|
||||
html <- BL.readFile $ webPath <> "/index.html"
|
||||
|
||||
-- "external" CA signing HTTP credentials
|
||||
Fingerprint fpHTTP <- loadFileFingerprint "tests/fixtures/ca.crt"
|
||||
let caHTTP = C.KeyHash fpHTTP
|
||||
manager <- H1.newManager H1.defaultManagerSettings
|
||||
H1.responseBody <$> H1.httpLbs "http://127.0.0.1:8000" manager `shouldReturn` html
|
||||
logDebug "Plain HTTP works"
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
let cfgHttp = defaultTransportClientConfig {alpn = Just ["h2"], useSNI = True}
|
||||
runTLSTransportClient defaultSupportedParamsHTTPS Nothing cfgHttp Nothing "localhost" "5223" (Just caHTTP) $ \tls -> do
|
||||
tlsALPN tls `shouldBe` Just "h2"
|
||||
case getCerts tls of
|
||||
X.Certificate {X.certPubKey = X.PubKeyRSA rsa} : _ca -> RSA.public_size rsa `shouldBe` 512
|
||||
leaf : _ -> error $ "Unexpected leaf cert: " <> show leaf
|
||||
[] -> error "Empty chain"
|
||||
|
||||
let h2cfg = HC.defaultHTTP2ClientConfig {HC.bodyHeadSize = 1024 * 1024}
|
||||
h2 <- either (error . show) pure =<< HC.attachHTTP2Client h2cfg "localhost" "5223" mempty 65536 tls
|
||||
let req = H2.requestNoBody "GET" "/" []
|
||||
HC.HTTP2Response {HC.respBody = HTTP2Body {bodyHead = shsBody}} <- either (error . show) pure =<< HC.sendRequest h2 req (Just 1000000)
|
||||
BL.fromStrict shsBody `shouldBe` html
|
||||
logDebug "Combined HTTPS works"
|
||||
|
||||
-- "local" CA signing SMP credentials
|
||||
Fingerprint fpSMP <- loadFileFingerprint (cfgPath <> "/ca.crt")
|
||||
let caSMP = C.KeyHash fpSMP
|
||||
let cfgSmp = defaultTransportClientConfig {alpn = Just ["smp/1"], useSNI = False}
|
||||
runTLSTransportClient defaultSupportedParams Nothing cfgSmp Nothing "localhost" "5223" (Just caSMP) $ \tls -> do
|
||||
tlsALPN tls `shouldBe` Just "smp/1"
|
||||
case getCerts tls of
|
||||
X.Certificate {X.certPubKey = X.PubKeyEd25519 _k} : _ca -> print _ca -- pure ()
|
||||
leaf : _ -> error $ "Unexpected leaf cert: " <> show leaf
|
||||
[] -> error "Empty chain"
|
||||
runRight_ . void $ smpClientHandshake tls Nothing caSMP supportedClientSMPRelayVRange
|
||||
logDebug "Combined SMP works"
|
||||
where
|
||||
getCerts :: TLS -> [X.Certificate]
|
||||
getCerts tls =
|
||||
let X.CertificateChain cc = tlsServerCerts tls
|
||||
in map (X.signedObject . X.getSigned) cc
|
||||
|
||||
ntfServerTest :: Bool -> IO ()
|
||||
ntfServerTest storeLog = do
|
||||
capture_ (withArgs (["init"] <> ["-l" | storeLog]) $ ntfServerCLI ntfCfgPath ntfLogPath)
|
||||
@@ -107,7 +195,7 @@ ntfServerTest storeLog = do
|
||||
r <- lines <$> capture_ (withArgs ["start"] $ (100000 `timeout` ntfServerCLI ntfCfgPath ntfLogPath) `catchAll_` pure (Just ()))
|
||||
r `shouldContain` ["SMP notifications server v" <> simplexMQVersion]
|
||||
r `shouldContain` (if storeLog then ["Store log: " <> ntfLogPath <> "/ntf-server-store.log"] else ["Store log disabled."])
|
||||
r `shouldContain` ["Listening on port 443 (TLS)..."]
|
||||
r `shouldContain` ["Serving SMP protocol on port 443 (TLS)..."]
|
||||
capture_ (withStdin "Y" . withArgs ["delete"] $ ntfServerCLI ntfCfgPath ntfLogPath)
|
||||
>>= (`shouldSatisfy` ("WARNING: deleting the server will make all queues inaccessible" `isPrefixOf`))
|
||||
doesFileExist (cfgPath <> "/ca.key") `shouldReturn` False
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module CoreTests.SOCKSSettings where
|
||||
|
||||
import Network.Socket (SockAddr (..), tupleToHostAddress)
|
||||
import Simplex.Messaging.Client
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ErrorType)
|
||||
import Simplex.Messaging.Transport.Client
|
||||
import Test.Hspec
|
||||
|
||||
socksSettingsTests :: Spec
|
||||
socksSettingsTests = do
|
||||
describe "hostMode and requiredHostMode settings" testHostMode
|
||||
describe "socksMode setting, independent of hostMode setting" testSocksMode
|
||||
describe "socks proxy address encoding" testSocksProxyEncoding
|
||||
|
||||
testPublicHost :: TransportHost
|
||||
testPublicHost = "smp.example.com"
|
||||
|
||||
testOnionHost :: TransportHost
|
||||
testOnionHost = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrst.onion"
|
||||
|
||||
testHostMode :: Spec
|
||||
testHostMode = do
|
||||
describe "requiredHostMode = False (default)" $ do
|
||||
it "without socks proxy, should choose onion host only with HMOnion" $ do
|
||||
chooseTransportHost @ErrorType defaultNetworkConfig [testPublicHost, testOnionHost] `shouldBe` Right testPublicHost
|
||||
chooseHost HMOnionViaSocks Nothing [testPublicHost, testOnionHost] `shouldBe` Right testPublicHost
|
||||
chooseHost HMOnion Nothing [testPublicHost, testOnionHost] `shouldBe` Right testOnionHost
|
||||
chooseHost HMPublic Nothing [testPublicHost, testOnionHost] `shouldBe` Right testPublicHost
|
||||
it "with socks proxy, should choose onion host with HMOnionViaSocks (default) and HMOnion" $ do
|
||||
chooseTransportHost @ErrorType defaultNetworkConfig {socksProxy = Just defaultSocksProxyWithAuth} [testPublicHost, testOnionHost] `shouldBe` Right testOnionHost
|
||||
chooseHost HMOnionViaSocks (Just defaultSocksProxyWithAuth) [testPublicHost, testOnionHost] `shouldBe` Right testOnionHost
|
||||
chooseHost HMOnion (Just defaultSocksProxyWithAuth) [testPublicHost, testOnionHost] `shouldBe` Right testOnionHost
|
||||
chooseHost HMPublic (Just defaultSocksProxyWithAuth) [testPublicHost, testOnionHost] `shouldBe` Right testPublicHost
|
||||
it "should choose any available host, if preferred not available" $ do
|
||||
chooseHost HMOnionViaSocks Nothing [testOnionHost] `shouldBe` Right testOnionHost
|
||||
chooseHost HMOnion Nothing [testPublicHost] `shouldBe` Right testPublicHost
|
||||
chooseHost HMPublic Nothing [testOnionHost] `shouldBe` Right testOnionHost
|
||||
chooseHost HMOnionViaSocks (Just defaultSocksProxyWithAuth) [testPublicHost] `shouldBe` Right testPublicHost
|
||||
chooseHost HMOnion (Just defaultSocksProxyWithAuth) [testPublicHost] `shouldBe` Right testPublicHost
|
||||
chooseHost HMPublic (Just defaultSocksProxyWithAuth) [testOnionHost] `shouldBe` Right testOnionHost
|
||||
describe "requiredHostMode = True" $ do
|
||||
it "should fail, if preferred host not available" $ do
|
||||
testOnionHost `incompatible` (HMOnionViaSocks, Nothing)
|
||||
testPublicHost `incompatible` (HMOnion, Nothing)
|
||||
testOnionHost `incompatible` (HMPublic, Nothing)
|
||||
testPublicHost `incompatible` (HMOnionViaSocks, Just defaultSocksProxyWithAuth)
|
||||
testPublicHost `incompatible` (HMOnion, Just defaultSocksProxyWithAuth)
|
||||
testOnionHost `incompatible` (HMPublic, Just defaultSocksProxyWithAuth)
|
||||
it "should choose preferred host, if available" $ do
|
||||
testPublicHost `compatible` (HMOnionViaSocks, Nothing)
|
||||
testOnionHost `compatible` (HMOnion, Nothing)
|
||||
testPublicHost `compatible` (HMPublic, Nothing)
|
||||
testOnionHost `compatible` (HMOnionViaSocks, Just defaultSocksProxyWithAuth)
|
||||
testOnionHost `compatible` (HMOnion, Just defaultSocksProxyWithAuth)
|
||||
testPublicHost `compatible` (HMPublic, Just defaultSocksProxyWithAuth)
|
||||
where
|
||||
chooseHost = chooseHostCfg defaultNetworkConfig
|
||||
host `incompatible` (hostMode, socksProxy) =
|
||||
chooseHostCfg defaultNetworkConfig {requiredHostMode = True} hostMode socksProxy [host] `shouldBe` Left PCEIncompatibleHost
|
||||
host `compatible` (hostMode, socksProxy) = do
|
||||
chooseHostCfg defaultNetworkConfig {requiredHostMode = True} hostMode socksProxy [host] `shouldBe` Right host
|
||||
chooseHostCfg defaultNetworkConfig {requiredHostMode = True} hostMode socksProxy [host, testPublicHost, testOnionHost] `shouldBe` Right host
|
||||
chooseHostCfg cfg hostMode socksProxy =
|
||||
chooseTransportHost @ErrorType cfg {hostMode, socksProxy}
|
||||
|
||||
testSocksMode :: Spec
|
||||
testSocksMode = do
|
||||
it "should not use SOCKS proxy if not specified" $ do
|
||||
transportSocksCfg defaultNetworkConfig testPublicHost `shouldBe` Nothing
|
||||
transportSocksCfg defaultNetworkConfig testOnionHost `shouldBe` Nothing
|
||||
transportSocks Nothing SMAlways testPublicHost `shouldBe` Nothing
|
||||
transportSocks Nothing SMAlways testOnionHost `shouldBe` Nothing
|
||||
transportSocks Nothing SMOnion testPublicHost `shouldBe` Nothing
|
||||
transportSocks Nothing SMOnion testOnionHost `shouldBe` Nothing
|
||||
it "should always use SOCKS proxy if specified and (socksMode = SMAlways or (socksMode = SMOnion and onion host))" $ do
|
||||
transportSocksCfg defaultNetworkConfig {socksProxy = Just defaultSocksProxyWithAuth} testPublicHost `shouldBe` Just defaultSocksProxy
|
||||
transportSocksCfg defaultNetworkConfig {socksProxy = Just defaultSocksProxyWithAuth} testOnionHost `shouldBe` Just defaultSocksProxy
|
||||
transportSocks (Just defaultSocksProxyWithAuth) SMAlways testPublicHost `shouldBe` Just defaultSocksProxy
|
||||
transportSocks (Just defaultSocksProxyWithAuth) SMAlways testOnionHost `shouldBe` Just defaultSocksProxy
|
||||
transportSocks (Just defaultSocksProxyWithAuth) SMOnion testPublicHost `shouldBe` Nothing
|
||||
transportSocks (Just defaultSocksProxyWithAuth) SMOnion testOnionHost `shouldBe` Just defaultSocksProxy
|
||||
where
|
||||
transportSocks proxy socksMode = transportSocksCfg defaultNetworkConfig {socksProxy = proxy, socksMode}
|
||||
transportSocksCfg cfg host =
|
||||
let TransportClientConfig {socksProxy} = transportClientConfig cfg host True
|
||||
in socksProxy
|
||||
|
||||
testSocksProxyEncoding :: Spec
|
||||
testSocksProxyEncoding = do
|
||||
it "should decode SOCKS proxy with isolate-by-auth mode" $ do
|
||||
let authIsolate proxy = Right $ SocksProxyWithAuth SocksIsolateByAuth proxy
|
||||
strDecode "" `shouldBe` authIsolate defaultSocksProxy
|
||||
strDecode ":9050" `shouldBe` authIsolate defaultSocksProxy
|
||||
strDecode ":8080" `shouldBe` authIsolate (SocksProxy $ SockAddrInet 8080 $ tupleToHostAddress defaultSocksHost)
|
||||
strDecode "127.0.0.1" `shouldBe` authIsolate defaultSocksProxy
|
||||
strDecode "1.1.1.1" `shouldBe` authIsolate (SocksProxy $ SockAddrInet 9050 $ tupleToHostAddress (1, 1, 1, 1))
|
||||
strDecode "::1" `shouldBe` authIsolate (SocksProxy $ SockAddrInet6 9050 0 (0, 0, 0, 1) 0)
|
||||
strDecode "[fd12:3456:789a:1::1]" `shouldBe` authIsolate (SocksProxy $ SockAddrInet6 9050 0 (0xfd123456, 0x789a0001, 0, 1) 0)
|
||||
strDecode "127.0.0.1:9050" `shouldBe` authIsolate defaultSocksProxy
|
||||
strDecode "127.0.0.1:8080" `shouldBe` authIsolate (SocksProxy $ SockAddrInet 8080 $ tupleToHostAddress defaultSocksHost)
|
||||
strDecode "[::1]:9050" `shouldBe` authIsolate (SocksProxy $ SockAddrInet6 9050 0 (0, 0, 0, 1) 0)
|
||||
strDecode "[::1]:8080" `shouldBe` authIsolate (SocksProxy $ SockAddrInet6 8080 0 (0, 0, 0, 1) 0)
|
||||
strDecode "[fd12:3456:789a:1::1]:8080" `shouldBe` authIsolate (SocksProxy $ SockAddrInet6 8080 0 (0xfd123456, 0x789a0001, 0, 1) 0)
|
||||
strEncode (SocksProxyWithAuth SocksIsolateByAuth defaultSocksProxy) `shouldBe` "127.0.0.1:9050"
|
||||
strEncode (SocksProxyWithAuth SocksIsolateByAuth (SocksProxy $ SockAddrInet6 9050 0 (0, 0, 0, 1) 0)) `shouldBe` "[::1]:9050"
|
||||
strEncode (SocksProxyWithAuth SocksIsolateByAuth (SocksProxy $ SockAddrInet6 8080 0 (0xfd123456, 0x789a0001, 0, 1) 0)) `shouldBe` "[fd12:3456:789a:1::1]:8080"
|
||||
it "should decode SOCKS proxy without credentials" $ do
|
||||
let authNull proxy = Right $ SocksProxyWithAuth SocksAuthNull proxy
|
||||
strDecode "@" `shouldBe` authNull defaultSocksProxy
|
||||
strDecode "@:9050" `shouldBe` authNull defaultSocksProxy
|
||||
strDecode "@127.0.0.1" `shouldBe` authNull defaultSocksProxy
|
||||
strDecode "@1.1.1.1" `shouldBe` authNull (SocksProxy $ SockAddrInet 9050 $ tupleToHostAddress (1, 1, 1, 1))
|
||||
strDecode "@127.0.0.1:9050" `shouldBe` authNull defaultSocksProxy
|
||||
strDecode "@[fd12:3456:789a:1::1]:8080" `shouldBe` authNull (SocksProxy $ SockAddrInet6 8080 0 (0xfd123456, 0x789a0001, 0, 1) 0)
|
||||
strEncode (SocksProxyWithAuth SocksAuthNull defaultSocksProxy) `shouldBe` "@127.0.0.1:9050"
|
||||
strEncode (SocksProxyWithAuth SocksAuthNull (SocksProxy $ SockAddrInet6 9050 0 (0, 0, 0, 1) 0)) `shouldBe` "@[::1]:9050"
|
||||
strEncode (SocksProxyWithAuth SocksAuthNull (SocksProxy $ SockAddrInet6 8080 0 (0xfd123456, 0x789a0001, 0, 1) 0)) `shouldBe` "@[fd12:3456:789a:1::1]:8080"
|
||||
it "should decode SOCKS proxy with credentials" $ do
|
||||
let auth = SocksAuthUsername {username = "user", password = "pass"}
|
||||
authUser proxy = Right $ SocksProxyWithAuth auth proxy
|
||||
strDecode "user:pass@" `shouldBe` authUser defaultSocksProxy
|
||||
strDecode "user:pass@:9050" `shouldBe` authUser defaultSocksProxy
|
||||
strDecode "user:pass@127.0.0.1" `shouldBe` authUser defaultSocksProxy
|
||||
strDecode "user:pass@127.0.0.1:9050" `shouldBe` authUser defaultSocksProxy
|
||||
strDecode "user:pass@fd12:3456:789a:1::1" `shouldBe` authUser (SocksProxy $ SockAddrInet6 9050 0 (0xfd123456, 0x789a0001, 0, 1) 0)
|
||||
strDecode "user:pass@[fd12:3456:789a:1::1]:8080" `shouldBe` authUser (SocksProxy $ SockAddrInet6 8080 0 (0xfd123456, 0x789a0001, 0, 1) 0)
|
||||
strEncode (SocksProxyWithAuth auth defaultSocksProxy) `shouldBe` "user:pass@127.0.0.1:9050"
|
||||
strEncode (SocksProxyWithAuth auth (SocksProxy $ SockAddrInet6 9050 0 (0, 0, 0, 1) 0)) `shouldBe` "user:pass@[::1]:9050"
|
||||
strEncode (SocksProxyWithAuth auth (SocksProxy $ SockAddrInet6 8080 0 (0xfd123456, 0x789a0001, 0, 1) 0)) `shouldBe` "user:pass@[fd12:3456:789a:1::1]:8080"
|
||||
+16
-12
@@ -45,7 +45,6 @@ import Simplex.Messaging.Transport.Client
|
||||
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), http2TLSParams)
|
||||
import Simplex.Messaging.Transport.HTTP2.Server
|
||||
import Simplex.Messaging.Transport.Server
|
||||
import qualified Simplex.Messaging.Transport.Server as Server
|
||||
import Test.Hspec
|
||||
import UnliftIO.Async
|
||||
import UnliftIO.Concurrent
|
||||
@@ -96,17 +95,19 @@ ntfServerCfg =
|
||||
subsBatchSize = 900,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
storeLogFile = Nothing,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
ntfCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
},
|
||||
-- stats config
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/ntf-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
ntfServerVRange = supportedServerNTFVRange,
|
||||
transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedNTFHandshakes}
|
||||
transportConfig = defaultTransportServerConfig
|
||||
}
|
||||
|
||||
ntfServerCfgVPrev :: NtfServerConfig
|
||||
@@ -121,10 +122,10 @@ ntfServerCfgVPrev =
|
||||
serverVRange' = serverVRange smpCfg'
|
||||
|
||||
withNtfServerStoreLog :: ATransport -> (ThreadId -> IO a) -> IO a
|
||||
withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t)]}
|
||||
withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t, False)]}
|
||||
|
||||
withNtfServerThreadOn :: ATransport -> ServiceName -> (ThreadId -> IO a) -> IO a
|
||||
withNtfServerThreadOn t port' = withNtfServerCfg ntfServerCfg {transports = [(port', t)]}
|
||||
withNtfServerThreadOn t port' = withNtfServerCfg ntfServerCfg {transports = [(port', t, False)]}
|
||||
|
||||
withNtfServerCfg :: HasCallStack => NtfServerConfig -> (ThreadId -> IO a) -> IO a
|
||||
withNtfServerCfg cfg@NtfServerConfig {transports} =
|
||||
@@ -185,9 +186,12 @@ apnsMockServerConfig =
|
||||
bufferSize = 16384,
|
||||
bodyHeadSize = 16384,
|
||||
serverSupported = http2TLSParams,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
https2Credentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
},
|
||||
transportConfig = defaultTransportServerConfig
|
||||
}
|
||||
|
||||
@@ -219,7 +223,7 @@ deriving instance ToJSON APNSErrorResponse
|
||||
|
||||
getAPNSMockServer :: HTTP2ServerConfig -> IO APNSMockServer
|
||||
getAPNSMockServer config@HTTP2ServerConfig {qSize} = do
|
||||
http2Server <- getHTTP2Server config
|
||||
http2Server <- getHTTP2Server config Nothing
|
||||
apnsQ <- newTBQueueIO qSize
|
||||
action <- async $ runAPNSMockServer apnsQ http2Server
|
||||
pure APNSMockServer {action, apnsQ, http2Server}
|
||||
|
||||
+23
-2
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
@@ -8,9 +9,11 @@ import AgentTests.FunctionalAPITests (runRight)
|
||||
import Control.Logger.Simple
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson as J
|
||||
import Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Transport (TSbChainKeys (..))
|
||||
import qualified Simplex.RemoteControl.Client as HC (RCHostClient (action))
|
||||
import qualified Simplex.RemoteControl.Client as RC
|
||||
import Simplex.RemoteControl.Discovery (mkLastLocalHost, preferAddress)
|
||||
@@ -74,7 +77,16 @@ testNewPairing = do
|
||||
logNote "c 3"
|
||||
Right (sessId, _tls, r') <- atomically $ takeTMVar r
|
||||
logNote "c 4"
|
||||
Right (_rcHostSession, _rcHelloBody, _hp') <- atomically $ takeTMVar r'
|
||||
Right (rcHostSession, _rcHelloBody, _hp') <- atomically $ takeTMVar r'
|
||||
let RCHostSession {tls, sessionKeys = HostSessKeys {chainKeys}} = rcHostSession
|
||||
TSbChainKeys {rcvKey, sndKey} = chainKeys
|
||||
sndKeyNonce <- atomically $ stateTVar sndKey C.sbcHkdf
|
||||
encCmd <- RC.rcEncryptBody sndKeyNonce "command message"
|
||||
RC.sendRCPacket tls $ LB.toStrict encCmd
|
||||
encResp <- RC.receiveRCPacket tls
|
||||
rcvKeyNonce <- atomically $ stateTVar rcvKey C.sbcHkdf
|
||||
resp <- RC.rcDecryptBody rcvKeyNonce $ LB.fromStrict encResp
|
||||
liftIO $ resp `shouldBe` "response message"
|
||||
logNote "c 5"
|
||||
threadDelay 250000
|
||||
logNote "ctrl: ciao"
|
||||
@@ -93,7 +105,16 @@ testNewPairing = do
|
||||
logNote "h 3"
|
||||
liftIO $ RC.confirmCtrlSession rcCtrlClient True
|
||||
logNote "h 4"
|
||||
Right (_rcCtrlSession, _rcCtrlPairing) <- atomically $ takeTMVar r'
|
||||
Right (rcCtrlSession, _rcCtrlPairing) <- atomically $ takeTMVar r'
|
||||
let RCCtrlSession {tls, sessionKeys = CtrlSessKeys {chainKeys}} = rcCtrlSession
|
||||
TSbChainKeys {rcvKey, sndKey} = chainKeys
|
||||
encCmd <- RC.receiveRCPacket tls
|
||||
rcvKeyNonce <- atomically $ stateTVar rcvKey C.sbcHkdf
|
||||
cmd <- RC.rcDecryptBody rcvKeyNonce $ LB.fromStrict encCmd
|
||||
liftIO $ cmd `shouldBe` "command message"
|
||||
sndKeyNonce <- atomically $ stateTVar sndKey C.sbcHkdf
|
||||
encResp <- RC.rcEncryptBody sndKeyNonce "response message"
|
||||
RC.sendRCPacket tls $ LB.toStrict encResp
|
||||
logNote "h 5"
|
||||
threadDelay 250000
|
||||
logNote "ctrl: adios"
|
||||
|
||||
+9
-6
@@ -27,7 +27,6 @@ import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client
|
||||
import qualified Simplex.Messaging.Transport.Client as Client
|
||||
import Simplex.Messaging.Transport.Server
|
||||
import qualified Simplex.Messaging.Transport.Server as Server
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
import System.Environment (lookupEnv)
|
||||
@@ -116,11 +115,15 @@ cfg =
|
||||
serverStatsLogFile = "tests/smp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
pendingENDInterval = 500000,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
smpCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
},
|
||||
httpCredentials = Nothing,
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedSMPHandshakes},
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
controlPort = Nothing,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 1}, -- seconds
|
||||
allowSMPProxy = False,
|
||||
@@ -164,7 +167,7 @@ withSmpServerStoreLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just tes
|
||||
withSmpServerConfigOn :: HasCallStack => ATransport -> ServerConfig -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withSmpServerConfigOn t cfg' port' =
|
||||
serverBracket
|
||||
(\started -> runSMPServerBlocking started cfg' {transports = [(port', t)]})
|
||||
(\started -> runSMPServerBlocking started cfg' {transports = [(port', t, False)]} Nothing)
|
||||
(threadDelay 10000)
|
||||
|
||||
withSmpServerThreadOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
|
||||
@@ -22,6 +22,7 @@ import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import SMPAgentClient
|
||||
import SMPClient
|
||||
import ServerTests (decryptMsgV3, sendRecv)
|
||||
@@ -150,12 +151,13 @@ deliverMessagesViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> S
|
||||
deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do
|
||||
g <- C.newRandom
|
||||
-- set up proxy
|
||||
pc' <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ())
|
||||
ts <- getCurrentTime
|
||||
pc' <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing ts (\_ -> pure ())
|
||||
pc <- either (fail . show) pure pc'
|
||||
THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc
|
||||
-- set up relay
|
||||
msgQ <- newTBQueueIO 1024
|
||||
rc' <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ())
|
||||
rc' <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) ts (\_ -> pure ())
|
||||
rc <- either (fail . show) pure rc'
|
||||
-- prepare receiving queue
|
||||
(rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g
|
||||
@@ -192,7 +194,8 @@ proxyConnectDeadRelay :: Int -> Int -> SMPServer -> IO ()
|
||||
proxyConnectDeadRelay n d proxyServ = do
|
||||
g <- C.newRandom
|
||||
-- set up proxy
|
||||
pc' <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ())
|
||||
ts <- getCurrentTime
|
||||
pc' <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing ts (\_ -> pure ())
|
||||
pc <- either (fail . show) pure pc'
|
||||
THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc
|
||||
-- get proxy session
|
||||
|
||||
@@ -385,6 +385,10 @@ testSwitchSub (ATransport t) =
|
||||
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, ACK mId3)
|
||||
(ok3, OK) #== "accepts ACK from the 2nd TCP connection"
|
||||
|
||||
Resp "cdab" _ OK <- signSendRecv rh1 rKey ("cdab", rId, DEL)
|
||||
Resp "" rId' DELD <- tGet1 rh2
|
||||
(rId', rId) #== "connection deleted event delivered to subscribed client"
|
||||
|
||||
1000 `timeout` tGet @SMPVersion @ErrorType @BrokerMsg rh1 >>= \case
|
||||
Nothing -> return ()
|
||||
Just _ -> error "nothing else is delivered to the 1st TCP connection"
|
||||
@@ -839,7 +843,8 @@ testMessageNotifications (ATransport t) =
|
||||
Resp "3a" _ OK <- signSendRecv rh rKey ("3a", rId, ACK mId1)
|
||||
Resp "" _ (NMSG _ _) <- tGet1 nh1
|
||||
Resp "4" _ OK <- signSendRecv nh2 nKey ("4", nId, NSUB)
|
||||
Resp "" _ END <- tGet1 nh1
|
||||
Resp "" nId2 END <- tGet1 nh1
|
||||
nId2 `shouldBe` nId
|
||||
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, _SEND' "hello again")
|
||||
Resp "" _ (Msg mId2 msg2) <- tGet1 rh
|
||||
Resp "5a" _ OK <- signSendRecv rh rKey ("5a", rId, ACK mId2)
|
||||
@@ -849,6 +854,8 @@ testMessageNotifications (ATransport t) =
|
||||
Nothing -> pure ()
|
||||
Just _ -> error "nothing else should be delivered to the 1st notifier's TCP connection"
|
||||
Resp "6" _ OK <- signSendRecv rh rKey ("6", rId, NDEL)
|
||||
Resp "" nId3 DELD <- tGet1 nh2
|
||||
nId3 `shouldBe` nId
|
||||
Resp "7" _ OK <- signSendRecv sh sKey ("7", sId, _SEND' "hello there")
|
||||
Resp "" _ (Msg mId3 msg3) <- tGet1 rh
|
||||
(dec mId3 msg3, Right "hello there") #== "delivered from queue again"
|
||||
|
||||
@@ -12,6 +12,7 @@ import CoreTests.CryptoFileTests
|
||||
import CoreTests.CryptoTests
|
||||
import CoreTests.EncodingTests
|
||||
import CoreTests.RetryIntervalTests
|
||||
import CoreTests.SOCKSSettings
|
||||
import CoreTests.TRcvQueuesTests
|
||||
import CoreTests.UtilTests
|
||||
import CoreTests.VersionRangeTests
|
||||
@@ -52,6 +53,7 @@ main = do
|
||||
describe "Encryption tests" cryptoTests
|
||||
describe "Encrypted files tests" cryptoFileTests
|
||||
describe "Retry interval tests" retryIntervalTests
|
||||
describe "SOCKS settings tests" socksSettingsTests
|
||||
describe "TRcvQueues tests" tRcvQueuesTests
|
||||
describe "Util tests" utilTests
|
||||
describe "SMP server via TLS" $ serverTests (transport @TLS)
|
||||
|
||||
+9
-8
@@ -24,7 +24,7 @@ import SMPClient (xit'')
|
||||
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.Server.Env (XFTPServerConfig (..))
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), supportedXFTPhandshakes)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (AUTH))
|
||||
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
|
||||
import Simplex.Messaging.Agent (AgentClient, testProtocolServer, xftpDeleteRcvFile, xftpDeleteSndFileInternal, xftpDeleteSndFileRemote, xftpReceiveFile, xftpSendDescription, xftpSendFile, xftpStartWorkers)
|
||||
@@ -37,6 +37,7 @@ import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Protocol (BasicAuth, ProtoServerWithAuth (..), ProtocolServer (..), XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
|
||||
import Simplex.Messaging.Transport (ALPN)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import System.Directory (doesDirectoryExist, doesFileExist, getFileSize, listDirectory, removeFile)
|
||||
import System.FilePath ((</>))
|
||||
@@ -257,11 +258,11 @@ testXFTPAgentSendReceiveMatrix = do
|
||||
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
|
||||
oldServer = Nothing
|
||||
newServer = Just supportedXFTPhandshakes
|
||||
run :: HasCallStack => Maybe [ALPN] -> AgentConfig -> AgentConfig -> IO ()
|
||||
run alpn sender receiver =
|
||||
withXFTPServerCfgALPN testXFTPServerConfig alpn $ \_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)
|
||||
@@ -429,7 +430,7 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
("", sfId', SFPROG _ _) <- sfGet sndr'
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
|
||||
threadDelay 100000
|
||||
threadDelay 200000
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
-- send file - should continue uploading with server up
|
||||
@@ -443,7 +444,7 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
pure rfd1
|
||||
|
||||
-- prefix path should be removed after sending file
|
||||
threadDelay 100000
|
||||
threadDelay 200000
|
||||
doesDirectoryExist prefixPath `shouldReturn` False
|
||||
doesFileExist encPath `shouldReturn` False
|
||||
|
||||
|
||||
+17
-12
@@ -8,6 +8,7 @@ module XFTPClient where
|
||||
|
||||
import Control.Concurrent (ThreadId, threadDelay)
|
||||
import Data.String (fromString)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Network.Socket (ServiceName)
|
||||
import SMPClient (serverBracket)
|
||||
import Simplex.FileTransfer.Client
|
||||
@@ -52,9 +53,12 @@ withXFTPServerStoreLogOn :: HasCallStack => (HasCallStack => ThreadId -> IO a) -
|
||||
withXFTPServerStoreLogOn = withXFTPServerCfg testXFTPServerConfig {storeLogFile = Just testXFTPLogFile, serverStatsBackupFile = Just testXFTPStatsBackupFile}
|
||||
|
||||
withXFTPServerCfg :: HasCallStack => XFTPServerConfig -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withXFTPServerCfg cfg =
|
||||
withXFTPServerCfg cfg = withXFTPServerCfgALPN cfg $ Just supportedXFTPhandshakes
|
||||
|
||||
withXFTPServerCfgALPN :: HasCallStack => XFTPServerConfig -> Maybe [ALPN] -> (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
withXFTPServerCfgALPN cfg alpn_ =
|
||||
serverBracket
|
||||
(`runXFTPServerBlocking` cfg)
|
||||
(\started -> runXFTPServerBlocking started cfg alpn_)
|
||||
(threadDelay 10000)
|
||||
|
||||
withXFTPServerThreadOn :: HasCallStack => (HasCallStack => ThreadId -> IO a) -> IO a
|
||||
@@ -97,10 +101,7 @@ testXFTPStatsBackupFile :: FilePath
|
||||
testXFTPStatsBackupFile = "tests/tmp/xftp-server-stats.log"
|
||||
|
||||
testXFTPServerConfig :: XFTPServerConfig
|
||||
testXFTPServerConfig = testXFTPServerConfig_ (Just supportedXFTPhandshakes)
|
||||
|
||||
testXFTPServerConfig_ :: Maybe [ALPN] -> XFTPServerConfig
|
||||
testXFTPServerConfig_ alpn =
|
||||
testXFTPServerConfig =
|
||||
XFTPServerConfig
|
||||
{ xftpPort = xftpTestPort,
|
||||
controlPort = Nothing,
|
||||
@@ -116,15 +117,18 @@ testXFTPServerConfig_ alpn =
|
||||
fileExpiration = Just defaultFileExpiration,
|
||||
fileTimeout = 10000000,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
xftpCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
},
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
transportConfig = defaultTransportServerConfig {alpn},
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
responseDelay = 0
|
||||
}
|
||||
|
||||
@@ -135,7 +139,8 @@ testXFTPClient :: HasCallStack => (HasCallStack => XFTPClient -> IO a) -> IO a
|
||||
testXFTPClient = testXFTPClientWith testXFTPClientConfig
|
||||
|
||||
testXFTPClientWith :: HasCallStack => XFTPClientConfig -> (HasCallStack => XFTPClient -> IO a) -> IO a
|
||||
testXFTPClientWith cfg client =
|
||||
getXFTPClient (1, testXFTPServer, Nothing) cfg (\_ -> pure ()) >>= \case
|
||||
testXFTPClientWith cfg client = do
|
||||
ts <- getCurrentTime
|
||||
getXFTPClient (1, testXFTPServer, Nothing) cfg ts (\_ -> pure ()) >>= \case
|
||||
Right c -> client c
|
||||
Left e -> error $ show e
|
||||
|
||||
@@ -18,6 +18,7 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.List (isInfixOf)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import ServerTests (logSize)
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Description (kb)
|
||||
@@ -220,7 +221,8 @@ testFileChunkExpiration = withXFTPServerCfg testXFTPServerConfig {fileExpiration
|
||||
testInactiveClientExpiration :: Expectation
|
||||
testInactiveClientExpiration = withXFTPServerCfg testXFTPServerConfig {inactiveClientExpiration} $ \_ -> runRight_ $ do
|
||||
disconnected <- newEmptyTMVarIO
|
||||
c <- ExceptT $ getXFTPClient (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> atomically $ putTMVar disconnected ())
|
||||
ts <- liftIO getCurrentTime
|
||||
c <- ExceptT $ getXFTPClient (1, testXFTPServer, Nothing) testXFTPClientConfig ts (\_ -> atomically $ putTMVar disconnected ())
|
||||
pingXFTP c
|
||||
liftIO $ do
|
||||
threadDelay 100000
|
||||
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDnTCCAx2gAwIBAgIUFhZZsKj9uBgGnUrr+Cf3XFf7t6IwBQYDK2VxMCoxFjAU
|
||||
BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjQwOTI2
|
||||
MTIyNTEyWhgPNDc2MjA4MjMxMjI1MTJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC
|
||||
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALB59b8oyxP5YtXI1kemBzJU
|
||||
Pt0xLN/Tmzdul283DhbNCJV+eUn4fNz+PjiRS/F2vZLb3WXInPi3bc57hw2Yu94o
|
||||
7MXH5DTWkaubNq0bV0Koi17zZBSCOOq+MbPN7bUT1sOwOHadLh3IWTfkz9EufowD
|
||||
ivpymNKWbeAHMXlXsBJnHfHuM05MWlP87PTHd3D7YQmmgbISgEGG4GchWBqnnCxx
|
||||
gOOa09f/n+gWJFbN3hkbVZKMEpT5gu9WWsgv9BDhJzcBSw13MMz0sByxYKzhwQBJ
|
||||
ikFz+16AttZ0ccoDaWwajZzK8+yfFv9T3b8kWmioHi2dw2vBgSove78liUqYCsOU
|
||||
Bt5MNk3P037KgSJPdp6azsF3bMKmPssEhT9vHMPgSkiBfmBlJ7dTTRd9dh/cLKIO
|
||||
AMzu4O+pEodIOJDXTARBE6VX1qoEZQuft5+ljVy4i9ySpmHnkxLocF40rKV1G0c5
|
||||
LnVNTtr5GokC9sfIXZPZw0EEpk3eAseNWccwuyRfHQfL6yjcDig2IdLvLVcm9JyA
|
||||
2P5QpP15EoA3Ow9uX8HmBbSFe1F35rqcNwY0lhDXEboSA/X4xDLnu4aVhNPiUnRq
|
||||
NXqVlgz5ybRAUHd8fDBwK8fT5VhvuEnCja7+8hVc33gK56vu+28ZMkN2Y4z0GNQd
|
||||
iamPUZJlUcCJzNI2cz27AgMBAAGjbzBtMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgPI
|
||||
MBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBSWiPT6Nl13/CTjaHCkHp17
|
||||
GWoyvzAfBgNVHSMEGDAWgBQcUJvR7mm26yxMQfCsWgbnwMmJVDAFBgMrZXEDcwDC
|
||||
DTbvSA61ydoRA8mTHFW1EYL+xfQjo0aH56N1Aqn47DzLGQZjP/fxoW929+Jwoiz0
|
||||
UgUtUAeFjgA9wfvDv7mMm/K4wqyiZzFuWVZdQV6AUwBJK0hN5qlXpvJzMKLrj3Ap
|
||||
dRELAgLJvC2e/xVc3dXSFwA=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBtjCCATagAwIBAgIUe2PryrWo0xXX9vcA3WfbCzcdmgAwBQYDK2VxMCoxFjAU
|
||||
BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjIwMTEx
|
||||
MTExNjM5WhgPNDc1OTEyMDgxMTE2MzlaMCoxFjAUBgNVBAMMDVNNUCBzZXJ2ZXIg
|
||||
Q0ExEDAOBgNVBAoMB1NpbXBsZVgwQzAFBgMrZXEDOgCAcvFwVicR+RLZpiEWPFNR
|
||||
XYTbf+mFcX1NHIyPQDugFwOCgqJAW1fsjYgFhtQJSMH/lc1N7clfm4CjUzBRMB0G
|
||||
A1UdDgQWBBQcUJvR7mm26yxMQfCsWgbnwMmJVDAfBgNVHSMEGDAWgBQcUJvR7mm2
|
||||
6yxMQfCsWgbnwMmJVDAPBgNVHRMBAf8EBTADAQH/MAUGAytlcQNzAAAP/hMPNxyW
|
||||
fyJi+iJViodU+C/aklnvHtjh5P3AbiVCSUfY6+PEdvkC8Ov0pBAYpYi5ukSNNVXl
|
||||
ABVRlipB+vOcLQStNyaZ7kXzQ2IO/0btmIidh+G6SP8I4aytYIYYcV5pEUZpG1L1
|
||||
57g8P29SDv81AA==
|
||||
-----END CERTIFICATE-----
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCwefW/KMsT+WLV
|
||||
yNZHpgcyVD7dMSzf05s3bpdvNw4WzQiVfnlJ+Hzc/j44kUvxdr2S291lyJz4t23O
|
||||
e4cNmLveKOzFx+Q01pGrmzatG1dCqIte82QUgjjqvjGzze21E9bDsDh2nS4dyFk3
|
||||
5M/RLn6MA4r6cpjSlm3gBzF5V7ASZx3x7jNOTFpT/Oz0x3dw+2EJpoGyEoBBhuBn
|
||||
IVgap5wscYDjmtPX/5/oFiRWzd4ZG1WSjBKU+YLvVlrIL/QQ4Sc3AUsNdzDM9LAc
|
||||
sWCs4cEASYpBc/tegLbWdHHKA2lsGo2cyvPsnxb/U92/JFpoqB4tncNrwYEqL3u/
|
||||
JYlKmArDlAbeTDZNz9N+yoEiT3aems7Bd2zCpj7LBIU/bxzD4EpIgX5gZSe3U00X
|
||||
fXYf3CyiDgDM7uDvqRKHSDiQ10wEQROlV9aqBGULn7efpY1cuIvckqZh55MS6HBe
|
||||
NKyldRtHOS51TU7a+RqJAvbHyF2T2cNBBKZN3gLHjVnHMLskXx0Hy+so3A4oNiHS
|
||||
7y1XJvScgNj+UKT9eRKANzsPbl/B5gW0hXtRd+a6nDcGNJYQ1xG6EgP1+MQy57uG
|
||||
lYTT4lJ0ajV6lZYM+cm0QFB3fHwwcCvH0+VYb7hJwo2u/vIVXN94Cuer7vtvGTJD
|
||||
dmOM9BjUHYmpj1GSZVHAiczSNnM9uwIDAQABAoICAAIOOHg0nO85RMTNItpjgeYY
|
||||
P0HGvIAk63rX4dqss9lhbQBie9B4HPzIjjEfMv13qj5VBtimllYNTXrEuSUzeCi6
|
||||
E7vyTpo+qv/YEHtUadb/2tzfe1BxjdyX0vfz+CtXbAeefH1O6mGrI/Uuo3Xmpc9p
|
||||
jJSmpg/DBl53Amm6xWLk6rq7dcNiWrfZS0T9xYFQmx7RlZwmct/ZqR56Zrw942ff
|
||||
Hkts9psniyeiHBr2cnpRrEJry69T0q6JIeP+5doWewCqzPl+9rMyKiT6RV3uJKpu
|
||||
Z7ZavthNl6Xj+FTDHdcGJ0v5Bg/llQ8Qb6f/FsLImM6IgBwlj4AXXMiP1SboolEo
|
||||
TDgt4DE0sd7o5ZU+5gjI2E9l1JLk68Rh64YIY1pr7CDURkWYwGyR3Bs9mG3RVKZr
|
||||
ANl08YeqtTH8LnqASJyKq6+xthDSCRbEP6uFM/Y5jjjbCWXELtaWqRYo1esHjPU2
|
||||
OfVI8tE13+ewLhjCUvarf9TA4Edkut7celuNgPsm58+cA5FSQiuuGrR6RoeOCYEd
|
||||
a9knZQriLebcHT8ifh1SfbuElhSMiSTUReEXzOEjs9+/kZ8BEQWWmspGVq/RiOoj
|
||||
jtPrDE11wqRjjK2SjLwFQ0NlHo+sUGxH7IJMVOHONcaFde87KTggjFr2HJgOtkYe
|
||||
zii/I6bVCH6IxKZ3jBqlAoIBAQD085iZEgp1uoXqnlNgL49qy8a2Zev5B3mQjS61
|
||||
1+LYurIKm2TnWFdUyrqOyY9EQ5mj7LbtntcIseW5gNPqHF9F8UQ3NfU1uXiKpBGF
|
||||
dLi/NGPPOoeep4GOOo7+TFluwPQILB7UPoLrU+cNcNt6V5FMKkRaANb/nxxPdWWS
|
||||
UPpCU2zoRNoeGEvXU3yPlhMJcCWYc6wP6YXClqrUsUmcX0x1MPY54ad18jxjR1P9
|
||||
msemV98tqI9/utjuL0sOIZKlfR225Bj3RQ6u1dPqwPaWVcXH+I4za/SYnJ8Ivrz4
|
||||
hokIHUPHbC1C/+wfVQhLU5Z9fT061IHKwmVX1NRC0aYdxm0tAoIBAQC4b7IiEcRW
|
||||
BgmBRM4/BgKCHoqZwEojZozYTBzsEQinRY5spfEow+ONUFYubnr4rDOHVTUMHStm
|
||||
GEvRfZyGMU7Xp0BkbVyMrrUCGhOLtIL7qLsN1ZryUGuZzFO0u7Q8lvYRKiuHtq1v
|
||||
QVqFzOVut+Wa0e//RCStrGlex6ZXpGrHf6EISc2jeDeLTcVcQkLY6pQoMtMFkjJi
|
||||
7l671AUA5ISqCiJv9DbL2XItZGw1N9zXXy4tLJGr4gOyX0+JE9FFyTUaC5Yeu8FO
|
||||
0qxop1hW/ekEYISMJtEvigphAhv5hShEUp5+ZqNcnUxj5FejFDfBi6sHT9vmcNwA
|
||||
RoYkzuvtpzeHAoIBACp0mitVvChhltpubKcMN0BcZ2mvyrGUARbz0XfFHlVQLpG2
|
||||
E0whvKk+pg0flExRpyyJV79hu4WPR/DaCmDWYBEAW0Fygbi5F9J3022dKHRDgVUm
|
||||
oOD3yXW8YpJi61FN8j4EX6eL2ictmKt0tyXCTbW00boD0T/m9QI0p9EvZeDfEs5D
|
||||
OMbkkSiWGM3ORihpnqqIyfbME9oBQUSyIb8PqXHadaLcoKjJvnu6ni0jiZ0kN9Nz
|
||||
FsQdv4GxAsJFQWSbhe3wJP+eoYfeGefjYBn4bdpWE1eIS5Gz+8CJRrmQn+mfIONM
|
||||
tZ+aOfPISjK8HyZK8bTjpkddYDFT+yJFshQRE1UCggEBAJC7rllIAf/Tqv/TY9pX
|
||||
N/6uQuvW1xcisaJHUGb8EwNY9SRTsITh/B74DTlQn5WnZKRt/DvuZBExPcY+wWcZ
|
||||
KJrY+BIXNAp+SzNEDVSTqjoctfVsS7Sd4WKG0qVAq3bkrGLZ6eENPNrSuVvIZ79T
|
||||
9o1g8+ooqnPTmbi0Cdg7AURe5pqfeA0xGL1roVX99YFNzEgjYi+8A2hZUOQqxGZn
|
||||
7aeWXmHmjl233P68EKJOnTIx0gXHNOVibq46Vyrl71LJS6+NqheiFVdqwbs6n3tc
|
||||
s9AogbuN9phMxkpMInHTyb6b6x6cItRZ6Al3tkIWao6qsOMDCziyFiLtNPWLn98W
|
||||
Wt8CggEBAPNpzv2HT1BkDOvWzPxKtKZg9dDRoPi12N2jVLoGqnrHf8r3+oqOYcHs
|
||||
zsi1QgXsGoTHFGJZZ9Op5FSlcffTKiIb9IJESfkSjp7njSvYeiKyTkYzlb/N42qy
|
||||
qgIph91xrT35NEMJQIvHX0wEFaJZ2BDdVRXgUo6cwhBJp5JjwQTwZ3msqCVC6wBk
|
||||
iJGL46LAE7/6YalcwlvBxMudW+NrZ8TRnzEOBMzgHf5K8e6sGfhllspCG0HmzyBX
|
||||
euEfBjGykCmlNTs55/p/4aXBY8ydJQK7o8aBlEgL3EYqoDTyq8kSar6O35rnXpP8
|
||||
mPykG5ZK8mWK1XSXXze7YGNUW1TjtmY=
|
||||
-----END PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user